@pagopa/dx-savemoney 0.3.2 → 0.4.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 +10 -0
- 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 +55 -37
- 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/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 +13 -0
- 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 +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.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 +9 -9
- 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 +12 -0
- package/src/azure/__tests__/report.test.ts +78 -0
- package/src/azure/analyzer.ts +67 -43
- package/src/azure/analyzers/registry.ts +8 -2
- package/src/azure/analyzers/types.ts +6 -0
- 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 +14 -0
- package/src/finding.ts +10 -3
- package/src/index.ts +3 -3
- package/src/types.ts +12 -0
|
@@ -9,6 +9,7 @@ import * as armResources from "@azure/arm-resources";
|
|
|
9
9
|
import { getLogger } from "@logtape/logtape";
|
|
10
10
|
|
|
11
11
|
import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
12
|
+
import type { PricingService } from "../pricing/pricing-service.js";
|
|
12
13
|
|
|
13
14
|
import { DEFAULT_THRESHOLDS } from "../../types.js";
|
|
14
15
|
import {
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
* @param computeClient - Azure Compute client for VM details
|
|
28
29
|
* @param timespanDays - Number of days to analyze metrics
|
|
29
30
|
* @param verbose - Whether verbose logging is enabled
|
|
31
|
+
* @param cache - Run-scoped metrics cache
|
|
32
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
30
33
|
* @returns Analysis result with cost risk and reason
|
|
31
34
|
*/
|
|
32
35
|
export async function analyzeVM(
|
|
@@ -37,6 +40,7 @@ export async function analyzeVM(
|
|
|
37
40
|
thresholds: Thresholds = DEFAULT_THRESHOLDS,
|
|
38
41
|
verbose = false,
|
|
39
42
|
cache?: MetricsCache,
|
|
43
|
+
pricing?: PricingService,
|
|
40
44
|
): Promise<AnalysisResult> {
|
|
41
45
|
verboseLogResourceStart(
|
|
42
46
|
verbose,
|
|
@@ -76,21 +80,35 @@ export async function analyzeVM(
|
|
|
76
80
|
);
|
|
77
81
|
|
|
78
82
|
if (vmStatus?.code === "PowerState/deallocated") {
|
|
79
|
-
const result =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const result = await enrichWithPricing(
|
|
84
|
+
{
|
|
85
|
+
costRisk,
|
|
86
|
+
reason: "VM is deallocated. ",
|
|
87
|
+
suspectedUnused: true,
|
|
88
|
+
},
|
|
89
|
+
resource,
|
|
90
|
+
computeClient,
|
|
91
|
+
resourceGroupName,
|
|
92
|
+
vmName,
|
|
93
|
+
pricing,
|
|
94
|
+
);
|
|
84
95
|
verboseLogAnalysisResult(verbose, result);
|
|
85
96
|
return result;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
if (vmStatus?.code === "PowerState/stopped") {
|
|
89
|
-
const result =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
const result = await enrichWithPricing(
|
|
101
|
+
{
|
|
102
|
+
costRisk,
|
|
103
|
+
reason: "VM is stopped. ",
|
|
104
|
+
suspectedUnused: true,
|
|
105
|
+
},
|
|
106
|
+
resource,
|
|
107
|
+
computeClient,
|
|
108
|
+
resourceGroupName,
|
|
109
|
+
vmName,
|
|
110
|
+
pricing,
|
|
111
|
+
);
|
|
94
112
|
verboseLogAnalysisResult(verbose, result);
|
|
95
113
|
return result;
|
|
96
114
|
}
|
|
@@ -127,7 +145,67 @@ export async function analyzeVM(
|
|
|
127
145
|
reason += `Low network traffic (${(networkIn / 1024 / 1024).toFixed(2)} MB/day avg). `;
|
|
128
146
|
}
|
|
129
147
|
|
|
130
|
-
const result =
|
|
148
|
+
const result = await enrichWithPricing(
|
|
149
|
+
{ costRisk, reason, suspectedUnused: reason.length > 0 },
|
|
150
|
+
resource,
|
|
151
|
+
computeClient,
|
|
152
|
+
resourceGroupName,
|
|
153
|
+
vmName,
|
|
154
|
+
pricing,
|
|
155
|
+
);
|
|
131
156
|
verboseLogAnalysisResult(verbose, result);
|
|
132
157
|
return result;
|
|
133
158
|
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Best-effort enrichment of a VM analysis result with the estimated
|
|
162
|
+
* monthly cost recoverable by removing/right-sizing the resource.
|
|
163
|
+
*
|
|
164
|
+
* No-op when:
|
|
165
|
+
* - the result is not flagged as suspected unused
|
|
166
|
+
* - no pricing facade was provided
|
|
167
|
+
* - the VM SKU or region cannot be determined
|
|
168
|
+
* - the Retail Prices lookup returns no matching meter
|
|
169
|
+
*
|
|
170
|
+
* Any error along the way is logged at warn level and swallowed so the
|
|
171
|
+
* main analysis never fails because of a pricing problem.
|
|
172
|
+
*/
|
|
173
|
+
async function enrichWithPricing(
|
|
174
|
+
result: AnalysisResult,
|
|
175
|
+
resource: armResources.GenericResource,
|
|
176
|
+
computeClient: ComputeManagementClient,
|
|
177
|
+
resourceGroupName: string,
|
|
178
|
+
vmName: string,
|
|
179
|
+
pricing?: PricingService,
|
|
180
|
+
): Promise<AnalysisResult> {
|
|
181
|
+
if (!pricing || !result.suspectedUnused) {
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const details = await computeClient.virtualMachines.get(
|
|
186
|
+
resourceGroupName,
|
|
187
|
+
vmName,
|
|
188
|
+
);
|
|
189
|
+
const armSkuName = details.hardwareProfile?.vmSize;
|
|
190
|
+
const armRegionName = details.location ?? resource.location;
|
|
191
|
+
if (!armSkuName || !armRegionName) {
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
const os = details.storageProfile?.osDisk?.osType?.toLowerCase();
|
|
195
|
+
const estimatedMonthlySavings = await pricing.resolveVm({
|
|
196
|
+
armRegionName,
|
|
197
|
+
armSkuName,
|
|
198
|
+
os: os === "windows" ? "windows" : "linux",
|
|
199
|
+
});
|
|
200
|
+
if (!estimatedMonthlySavings) {
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
return { ...result, estimatedMonthlySavings };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
206
|
+
logger.warn(
|
|
207
|
+
`Failed to enrich VM ${vmName} with pricing: ${error instanceof Error ? error.message : error}`,
|
|
208
|
+
);
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/azure/types.ts
CHANGED
|
@@ -27,6 +27,12 @@ export type AzureConfig = BaseConfig & {
|
|
|
27
27
|
* If omitted, all resources are analyzed.
|
|
28
28
|
*/
|
|
29
29
|
filterTags?: Map<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Runtime-only pricing enrichment switch. This is intentionally not exposed
|
|
32
|
+
* in YAML config: the Retail Prices lookup uses internal defaults unless the
|
|
33
|
+
* CLI disables it for offline/debug runs.
|
|
34
|
+
*/
|
|
35
|
+
pricing?: PricingConfig;
|
|
30
36
|
/**
|
|
31
37
|
* Which finding sources to include in the run.
|
|
32
38
|
* - `"custom"` → enables the per-resource analyzer plugins
|
|
@@ -83,3 +89,11 @@ export type AzureResourceReport = {
|
|
|
83
89
|
* filter for Azure runs and would silently produce an empty report.
|
|
84
90
|
*/
|
|
85
91
|
export type AzureSource = "advisor" | "custom";
|
|
92
|
+
|
|
93
|
+
export type PricingConfig = {
|
|
94
|
+
/**
|
|
95
|
+
* Runtime switch used by the CLI `--no-pricing` flag. Pricing otherwise
|
|
96
|
+
* stays enabled with internal defaults for currency and cache settings.
|
|
97
|
+
*/
|
|
98
|
+
enabled?: boolean;
|
|
99
|
+
};
|
package/src/finding.ts
CHANGED
|
@@ -32,9 +32,9 @@ export type Finding = {
|
|
|
32
32
|
*/
|
|
33
33
|
code: string;
|
|
34
34
|
/**
|
|
35
|
-
* Estimated monthly
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Estimated monthly saving for this specific finding. Populated by Advisor
|
|
36
|
+
* recommendations; custom Retail Prices estimates stay at resource level
|
|
37
|
+
* because they represent cost at risk, not a per-finding saving.
|
|
38
38
|
*/
|
|
39
39
|
estimatedMonthlySavings?: Money;
|
|
40
40
|
/**
|
|
@@ -128,6 +128,13 @@ export function findingsFromAnalysisResult(args: {
|
|
|
128
128
|
if (sentences.length === 0) {
|
|
129
129
|
return [];
|
|
130
130
|
}
|
|
131
|
+
// We intentionally do NOT propagate any monetary estimate down to the
|
|
132
|
+
// individual findings: a per-resource cost (e.g. €29.94/mo for a VM)
|
|
133
|
+
// attached to a sentence like "No tags found" would falsely suggest
|
|
134
|
+
// "add the missing tags to save €29.94". The resource-level cost is
|
|
135
|
+
// carried instead by `AnalysisResult.estimatedMonthlySavings` and
|
|
136
|
+
// surfaced by the report layer as a per-resource header — see
|
|
137
|
+
// `azure/report.ts`.
|
|
131
138
|
return sentences.map((sentence) => ({
|
|
132
139
|
category: "cost" as const,
|
|
133
140
|
code: code ?? "custom.unknown",
|
package/src/index.ts
CHANGED
|
@@ -21,14 +21,13 @@ export {
|
|
|
21
21
|
} from "./azure/analyzers/index.js";
|
|
22
22
|
|
|
23
23
|
// Export common types
|
|
24
|
-
export type { AzureConfig, AzureSource } from "./azure/types.js";
|
|
24
|
+
export type { AzureConfig, AzureSource, PricingConfig } from "./azure/types.js";
|
|
25
|
+
export { type MetricsCache, type MonitorClientLike } from "./azure/utils.js";
|
|
25
26
|
|
|
26
27
|
// Export Azure module
|
|
27
28
|
import * as azureModule from "./azure/index.js";
|
|
28
29
|
export const azure = azureModule;
|
|
29
30
|
|
|
30
|
-
export { type MetricsCache, type MonitorClientLike } from "./azure/utils.js";
|
|
31
|
-
|
|
32
31
|
// Phase 0: unified Finding model and analyzer plugin layer
|
|
33
32
|
export {
|
|
34
33
|
type Finding,
|
|
@@ -38,6 +37,7 @@ export {
|
|
|
38
37
|
type Money,
|
|
39
38
|
type ResourceReport,
|
|
40
39
|
} from "./finding.js";
|
|
40
|
+
|
|
41
41
|
export * from "./types.js";
|
|
42
42
|
|
|
43
43
|
import type { AzureConfig } from "./azure/types.js";
|
package/src/types.ts
CHANGED
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
* Common types shared across all cloud providers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { Money } from "./finding.js";
|
|
6
|
+
|
|
5
7
|
import { ThresholdsSchema } from "./schema.js";
|
|
6
8
|
|
|
7
9
|
export type AnalysisResult = {
|
|
8
10
|
costRisk: CostRisk;
|
|
11
|
+
/**
|
|
12
|
+
* Estimated monthly resource cost at risk. Custom pricing-enriched
|
|
13
|
+
* analyzers populate it when a Retail Prices lookup succeeds; the report
|
|
14
|
+
* renders it once per resource instead of treating it as a per-finding
|
|
15
|
+
* saving.
|
|
16
|
+
*/
|
|
17
|
+
estimatedMonthlySavings?: Money;
|
|
9
18
|
reason: string;
|
|
10
19
|
suspectedUnused: boolean;
|
|
11
20
|
};
|
|
@@ -41,6 +50,9 @@ export function mergeResults(
|
|
|
41
50
|
): AnalysisResult {
|
|
42
51
|
return {
|
|
43
52
|
costRisk: specificResult.costRisk,
|
|
53
|
+
estimatedMonthlySavings:
|
|
54
|
+
specificResult.estimatedMonthlySavings ??
|
|
55
|
+
baseResult.estimatedMonthlySavings,
|
|
44
56
|
reason: baseResult.reason + specificResult.reason,
|
|
45
57
|
suspectedUnused:
|
|
46
58
|
baseResult.suspectedUnused || specificResult.suspectedUnused,
|