@pagopa/dx-savemoney 0.1.1 → 0.1.3
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 +58 -9
- package/dist/azure/analyzer.d.ts +3 -1
- package/dist/azure/analyzer.d.ts.map +1 -1
- package/dist/azure/analyzer.js +16 -3
- package/dist/azure/analyzer.js.map +1 -1
- package/dist/azure/resources/container-app.d.ts +19 -0
- package/dist/azure/resources/container-app.d.ts.map +1 -0
- package/dist/azure/resources/container-app.js +113 -0
- package/dist/azure/resources/container-app.js.map +1 -0
- package/dist/azure/resources/index.d.ts +2 -0
- package/dist/azure/resources/index.d.ts.map +1 -1
- package/dist/azure/resources/index.js +2 -0
- package/dist/azure/resources/index.js.map +1 -1
- package/dist/azure/resources/public-ip.js +4 -4
- package/dist/azure/resources/public-ip.js.map +1 -1
- package/dist/azure/resources/static-web-app.d.ts +17 -0
- package/dist/azure/resources/static-web-app.d.ts.map +1 -0
- package/dist/azure/resources/static-web-app.js +60 -0
- package/dist/azure/resources/static-web-app.js.map +1 -0
- package/dist/azure/resources/storage.js +4 -4
- package/dist/azure/resources/storage.js.map +1 -1
- package/dist/azure/resources/vm.js +4 -4
- package/dist/azure/resources/vm.js.map +1 -1
- package/dist/azure/utils.d.ts +24 -0
- package/dist/azure/utils.d.ts.map +1 -1
- package/dist/azure/utils.js +72 -20
- package/dist/azure/utils.js.map +1 -1
- package/package.json +5 -4
- package/src/azure/analyzer.ts +31 -0
- package/src/azure/resources/container-app.ts +247 -0
- package/src/azure/resources/index.ts +2 -0
- package/src/azure/resources/public-ip.ts +4 -4
- package/src/azure/resources/static-web-app.ts +108 -0
- package/src/azure/resources/storage.ts +4 -4
- package/src/azure/resources/vm.ts +4 -4
- package/src/azure/utils.ts +109 -26
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/azure/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/azure/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,KAAK,eAAe,GAAG;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,eAAe,EAAE,EAC7B,WAAW,EAAE,MAAM,GAClB,IAAI,GAAG,MAAM,CAiCf;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAC3B,WAAW,EAAE,MAAM,GAClB,IAAI,GAAG,MAAM,CA6Bf;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAsCxB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,QAUjB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,QAYvB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,QASrB"}
|
package/dist/azure/utils.js
CHANGED
|
@@ -2,6 +2,69 @@
|
|
|
2
2
|
* Azure utility functions for debugging and metrics
|
|
3
3
|
*/
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
|
+
/**
|
|
6
|
+
* Aggregates metric data points based on aggregation type.
|
|
7
|
+
*
|
|
8
|
+
* @param dataPoints - Array of metric data points
|
|
9
|
+
* @param aggregation - The aggregation type (e.g., "Average", "Total")
|
|
10
|
+
* @returns The aggregated value or null if unavailable
|
|
11
|
+
*/
|
|
12
|
+
export function aggregateDataPoints(dataPoints, aggregation) {
|
|
13
|
+
const aggregationLower = aggregation.toLowerCase();
|
|
14
|
+
// Get all non-null values from the data points
|
|
15
|
+
const values = dataPoints
|
|
16
|
+
.map((dataPoint) => extractAggregatedValue(dataPoint, aggregation))
|
|
17
|
+
.filter((v) => v !== null);
|
|
18
|
+
if (values.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (aggregationLower === "total" || aggregationLower === "count") {
|
|
22
|
+
// Sum all values for Total/Count
|
|
23
|
+
return values.reduce((sum, v) => sum + v, 0);
|
|
24
|
+
}
|
|
25
|
+
if (aggregationLower === "average") {
|
|
26
|
+
// Calculate the average of all values
|
|
27
|
+
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
28
|
+
}
|
|
29
|
+
if (aggregationLower === "maximum") {
|
|
30
|
+
// Find the maximum value
|
|
31
|
+
return Math.max(...values);
|
|
32
|
+
}
|
|
33
|
+
if (aggregationLower === "minimum") {
|
|
34
|
+
// Find the minimum value
|
|
35
|
+
return Math.min(...values);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extracts the aggregated value from metric data.
|
|
41
|
+
*
|
|
42
|
+
* @param metricData - The metric data point
|
|
43
|
+
* @param aggregation - The aggregation type (e.g., "Average", "Total")
|
|
44
|
+
* @returns The aggregated value or null if unavailable
|
|
45
|
+
*/
|
|
46
|
+
export function extractAggregatedValue(metricData, aggregation) {
|
|
47
|
+
const aggregationLower = aggregation.toLowerCase();
|
|
48
|
+
if (aggregationLower === "average" &&
|
|
49
|
+
typeof metricData.average === "number") {
|
|
50
|
+
return metricData.average;
|
|
51
|
+
}
|
|
52
|
+
if (aggregationLower === "total" && typeof metricData.total === "number") {
|
|
53
|
+
return metricData.total;
|
|
54
|
+
}
|
|
55
|
+
if (aggregationLower === "minimum" &&
|
|
56
|
+
typeof metricData.minimum === "number") {
|
|
57
|
+
return metricData.minimum;
|
|
58
|
+
}
|
|
59
|
+
if (aggregationLower === "maximum" &&
|
|
60
|
+
typeof metricData.maximum === "number") {
|
|
61
|
+
return metricData.maximum;
|
|
62
|
+
}
|
|
63
|
+
if (aggregationLower === "count" && typeof metricData.count === "number") {
|
|
64
|
+
return metricData.count;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
5
68
|
/**
|
|
6
69
|
* Fetches a specific metric for a resource from Azure Monitor.
|
|
7
70
|
*
|
|
@@ -20,30 +83,19 @@ export async function getMetric(monitorClient, resourceId, metricName, aggregati
|
|
|
20
83
|
metricnames: metricName,
|
|
21
84
|
timespan,
|
|
22
85
|
});
|
|
23
|
-
|
|
24
|
-
if (!metricData) {
|
|
86
|
+
if (result.value.length === 0) {
|
|
25
87
|
return null;
|
|
26
88
|
}
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
return metricData.average;
|
|
31
|
-
}
|
|
32
|
-
if (aggregationLower === "total" && typeof metricData.total === "number") {
|
|
33
|
-
return metricData.total;
|
|
34
|
-
}
|
|
35
|
-
if (aggregationLower === "minimum" &&
|
|
36
|
-
typeof metricData.minimum === "number") {
|
|
37
|
-
return metricData.minimum;
|
|
38
|
-
}
|
|
39
|
-
if (aggregationLower === "maximum" &&
|
|
40
|
-
typeof metricData.maximum === "number") {
|
|
41
|
-
return metricData.maximum;
|
|
89
|
+
const metric = result.value[0];
|
|
90
|
+
if (!metric.timeseries || metric.timeseries.length === 0) {
|
|
91
|
+
return null;
|
|
42
92
|
}
|
|
43
|
-
|
|
44
|
-
|
|
93
|
+
const timeserie = metric.timeseries[0];
|
|
94
|
+
if (!timeserie.data || timeserie.data.length === 0) {
|
|
95
|
+
return null;
|
|
45
96
|
}
|
|
46
|
-
|
|
97
|
+
const aggregatedValue = aggregateDataPoints(timeserie.data, aggregation);
|
|
98
|
+
return aggregatedValue;
|
|
47
99
|
}
|
|
48
100
|
catch (error) {
|
|
49
101
|
const logger = getLogger(["savemoney", "azure", "metrics"]);
|
package/dist/azure/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/azure/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/azure/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAY7C;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAA6B,EAC7B,WAAmB;IAEnB,MAAM,gBAAgB,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IAEnD,+CAA+C;IAC/C,MAAM,MAAM,GAAG,UAAU;SACtB,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,sBAAsB,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;SAClE,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAE1C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,gBAAgB,KAAK,OAAO,IAAI,gBAAgB,KAAK,OAAO,EAAE,CAAC;QACjE,iCAAiC;QACjC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,sCAAsC;QACtC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IAC/D,CAAC;IAED,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,yBAAyB;QACzB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,yBAAyB;QACzB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAA2B,EAC3B,WAAmB;IAEnB,MAAM,gBAAgB,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IAEnD,IACE,gBAAgB,KAAK,SAAS;QAC9B,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ,EACtC,CAAC;QACD,OAAO,UAAU,CAAC,OAAO,CAAC;IAC5B,CAAC;IACD,IAAI,gBAAgB,KAAK,OAAO,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACzE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC1B,CAAC;IACD,IACE,gBAAgB,KAAK,SAAS;QAC9B,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ,EACtC,CAAC;QACD,OAAO,UAAU,CAAC,OAAO,CAAC;IAC5B,CAAC;IACD,IACE,gBAAgB,KAAK,SAAS;QAC9B,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ,EACtC,CAAC;QACD,OAAO,UAAU,CAAC,OAAO,CAAC;IAC5B,CAAC;IACD,IAAI,gBAAgB,KAAK,OAAO,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACzE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC1B,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,aAA4B,EAC5B,UAAkB,EAClB,UAAkB,EAClB,WAAmB,EACnB,YAAoB;IAEpB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,YAAY,GAAG,CAAC;QACrC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE;YAC1D,WAAW;YACX,WAAW,EAAE,UAAU;YACvB,QAAQ;SACT,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAE/B,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAEvC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,eAAe,GAAG,mBAAmB,CACzC,SAAS,CAAC,IAAyB,EACnC,WAAW,CACZ,CAAC;QAEF,OAAO,eAAe,CAAC;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,CACV,0BAA0B,UAAU,iBAAiB,UAAU,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC7H,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,OAAgB,EAChB,OAAe,EACf,MAAgB;IAEhB,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAgB,EAChB,MAAsB;IAEtB,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACtC,MAAM,CAAC,KAAK,CAAC,iBAAiB,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC/D,MAAM,CAAC,KAAK,CACV,wBAAwB,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAChE,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,cAAc,MAAM,CAAC,MAAM,IAAI,iBAAiB,EAAE,CAAC,CAAC;QACjE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAgB,EAChB,YAAoB,EACpB,YAAoB;IAEpB,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,iBAAiB,YAAY,EAAE,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,YAAY,YAAY,EAAE,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagopa/dx-savemoney",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Azure resource analyzer for finding unused or cost-inefficient resources.",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"DX"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@azure/arm-appcontainers": "^3.0.0",
|
|
30
31
|
"@azure/arm-appservice": "^17.0.0",
|
|
31
32
|
"@azure/arm-compute": "^23.1.0",
|
|
32
33
|
"@azure/arm-monitor": "^7.0.0",
|
|
@@ -37,13 +38,13 @@
|
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@tsconfig/node22": "22.0.2",
|
|
40
|
-
"@types/node": "^22.
|
|
41
|
+
"@types/node": "^22.19.1",
|
|
41
42
|
"@vitest/coverage-v8": "^3.2.4",
|
|
42
|
-
"eslint": "^9.
|
|
43
|
+
"eslint": "^9.39.1",
|
|
43
44
|
"prettier": "3.6.2",
|
|
44
45
|
"typescript": "~5.8.3",
|
|
45
46
|
"vitest": "^3.2.4",
|
|
46
|
-
"@pagopa/eslint-config": "^5.1.
|
|
47
|
+
"@pagopa/eslint-config": "^5.1.1"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "rm -rf dist && tsc",
|
package/src/azure/analyzer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Azure resource analyzer - Main orchestration logic
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { ContainerAppsAPIClient } from "@azure/arm-appcontainers";
|
|
5
6
|
import { WebSiteManagementClient } from "@azure/arm-appservice";
|
|
6
7
|
import { ComputeManagementClient } from "@azure/arm-compute";
|
|
7
8
|
import { MonitorClient } from "@azure/arm-monitor";
|
|
@@ -16,10 +17,12 @@ import { type AnalysisResult, mergeResults } from "../types.js";
|
|
|
16
17
|
import { generateReport } from "./report.js";
|
|
17
18
|
import {
|
|
18
19
|
analyzeAppServicePlan,
|
|
20
|
+
analyzeContainerApp,
|
|
19
21
|
analyzeDisk,
|
|
20
22
|
analyzeNic,
|
|
21
23
|
analyzePrivateEndpoint,
|
|
22
24
|
analyzePublicIp,
|
|
25
|
+
analyzeStaticSite,
|
|
23
26
|
analyzeStorageAccount,
|
|
24
27
|
analyzeVM,
|
|
25
28
|
} from "./resources/index.js";
|
|
@@ -58,6 +61,10 @@ export async function analyzeAzureResources(
|
|
|
58
61
|
credential,
|
|
59
62
|
subscriptionId.trim(),
|
|
60
63
|
);
|
|
64
|
+
const containerAppsClient = new ContainerAppsAPIClient(
|
|
65
|
+
credential,
|
|
66
|
+
subscriptionId.trim(),
|
|
67
|
+
);
|
|
61
68
|
|
|
62
69
|
// Use the async iterator to avoid memory explosion for large environments
|
|
63
70
|
for await (const resource of resourceClient.resources.list()) {
|
|
@@ -67,6 +74,7 @@ export async function analyzeAzureResources(
|
|
|
67
74
|
computeClient,
|
|
68
75
|
networkClient,
|
|
69
76
|
webSiteClient,
|
|
77
|
+
containerAppsClient,
|
|
70
78
|
config.preferredLocation,
|
|
71
79
|
config.timespanDays,
|
|
72
80
|
config.verbose || false,
|
|
@@ -104,6 +112,7 @@ export async function analyzeAzureResources(
|
|
|
104
112
|
* @param computeClient - Azure Compute client
|
|
105
113
|
* @param networkClient - Azure Network client
|
|
106
114
|
* @param webSiteClient - Azure Web Site client
|
|
115
|
+
* @param containerAppsClient - Azure Container Apps client
|
|
107
116
|
* @param preferredLocation - Preferred Azure location
|
|
108
117
|
* @param timespanDays - Number of days to analyze metrics
|
|
109
118
|
* @param verbose - Whether verbose logging is enabled
|
|
@@ -115,6 +124,7 @@ export async function analyzeResource(
|
|
|
115
124
|
computeClient: ComputeManagementClient,
|
|
116
125
|
networkClient: NetworkManagementClient,
|
|
117
126
|
webSiteClient: WebSiteManagementClient,
|
|
127
|
+
containerAppsClient: ContainerAppsAPIClient,
|
|
118
128
|
preferredLocation: string,
|
|
119
129
|
timespanDays: number,
|
|
120
130
|
verbose = false,
|
|
@@ -134,6 +144,17 @@ export async function analyzeResource(
|
|
|
134
144
|
|
|
135
145
|
// Route to type-specific analysis hooks
|
|
136
146
|
switch (type) {
|
|
147
|
+
case "microsoft.app/containerapps": {
|
|
148
|
+
const containerAppResult = await analyzeContainerApp(
|
|
149
|
+
resource,
|
|
150
|
+
containerAppsClient,
|
|
151
|
+
monitorClient,
|
|
152
|
+
timespanDays,
|
|
153
|
+
verbose,
|
|
154
|
+
);
|
|
155
|
+
result = mergeResults(result, containerAppResult);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
137
158
|
case "microsoft.compute/disks": {
|
|
138
159
|
const diskResult = await analyzeDisk(resource, computeClient, verbose);
|
|
139
160
|
result = mergeResults(result, diskResult);
|
|
@@ -196,6 +217,16 @@ export async function analyzeResource(
|
|
|
196
217
|
result = mergeResults(result, aspResult);
|
|
197
218
|
break;
|
|
198
219
|
}
|
|
220
|
+
case "microsoft.web/staticsites": {
|
|
221
|
+
const staticSiteResult = await analyzeStaticSite(
|
|
222
|
+
resource,
|
|
223
|
+
monitorClient,
|
|
224
|
+
timespanDays,
|
|
225
|
+
verbose,
|
|
226
|
+
);
|
|
227
|
+
result = mergeResults(result, staticSiteResult);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
199
230
|
default:
|
|
200
231
|
result.reason += "No specific analysis for this resource type. ";
|
|
201
232
|
break;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Container App analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ContainerAppsAPIClient } from "@azure/arm-appcontainers";
|
|
6
|
+
import type { MonitorClient } from "@azure/arm-monitor";
|
|
7
|
+
|
|
8
|
+
import * as armResources from "@azure/arm-resources";
|
|
9
|
+
import { getLogger } from "@logtape/logtape";
|
|
10
|
+
|
|
11
|
+
import type { AnalysisResult } from "../../types.js";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getMetric,
|
|
15
|
+
verboseLog,
|
|
16
|
+
verboseLogAnalysisResult,
|
|
17
|
+
verboseLogResourceStart,
|
|
18
|
+
} from "../utils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Analyzes an Azure Container App for potential cost optimization.
|
|
22
|
+
*
|
|
23
|
+
* @param resource - The Azure resource object
|
|
24
|
+
* @param containerAppsClient - Azure Container Apps client for details
|
|
25
|
+
* @param monitorClient - Azure Monitor client for metrics
|
|
26
|
+
* @param timespanDays - Number of days to analyze metrics
|
|
27
|
+
* @param verbose - Enable verbose logging
|
|
28
|
+
* @returns Analysis result with cost risk and reason
|
|
29
|
+
*/
|
|
30
|
+
export async function analyzeContainerApp(
|
|
31
|
+
resource: armResources.GenericResource,
|
|
32
|
+
containerAppsClient: ContainerAppsAPIClient,
|
|
33
|
+
monitorClient: MonitorClient,
|
|
34
|
+
timespanDays: number,
|
|
35
|
+
verbose = false,
|
|
36
|
+
): Promise<AnalysisResult> {
|
|
37
|
+
verboseLogResourceStart(
|
|
38
|
+
verbose,
|
|
39
|
+
resource.name || "unknown",
|
|
40
|
+
"Container App (Microsoft.App/containerApps)",
|
|
41
|
+
);
|
|
42
|
+
verboseLog(verbose, "Resource details:", resource);
|
|
43
|
+
|
|
44
|
+
const costRisk: "high" | "low" | "medium" = "medium";
|
|
45
|
+
let reason = "";
|
|
46
|
+
|
|
47
|
+
if (!resource.id) {
|
|
48
|
+
return {
|
|
49
|
+
costRisk,
|
|
50
|
+
reason: "Resource ID is missing.",
|
|
51
|
+
suspectedUnused: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resourceParts = resource.id.split("/");
|
|
56
|
+
const resourceGroupName = resourceParts[4];
|
|
57
|
+
const containerAppName = resourceParts[8];
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const appDetails = await containerAppsClient.containerApps.get(
|
|
61
|
+
resourceGroupName,
|
|
62
|
+
containerAppName,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
verboseLog(verbose, "Container App API details:", appDetails);
|
|
66
|
+
|
|
67
|
+
reason = checkRunningStatus(
|
|
68
|
+
{
|
|
69
|
+
properties: {
|
|
70
|
+
provisioningState: appDetails.provisioningState,
|
|
71
|
+
runningStatus: appDetails.runningStatus,
|
|
72
|
+
template: appDetails.template,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
reason,
|
|
76
|
+
verbose,
|
|
77
|
+
);
|
|
78
|
+
reason = await checkResourceMetrics(
|
|
79
|
+
resource,
|
|
80
|
+
monitorClient,
|
|
81
|
+
timespanDays,
|
|
82
|
+
reason,
|
|
83
|
+
verbose,
|
|
84
|
+
);
|
|
85
|
+
reason = await checkNetworkMetrics(
|
|
86
|
+
resource,
|
|
87
|
+
monitorClient,
|
|
88
|
+
timespanDays,
|
|
89
|
+
reason,
|
|
90
|
+
verbose,
|
|
91
|
+
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
94
|
+
logger.warn(
|
|
95
|
+
`Failed to get Container App details for ${containerAppName}: ${error instanceof Error ? error.message : error}`,
|
|
96
|
+
);
|
|
97
|
+
reason += "Could not retrieve detailed Container App information. ";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const suspectedUnused = reason.length > 0;
|
|
101
|
+
const result = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
102
|
+
verboseLogAnalysisResult(verbose, result);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks network traffic metrics for a Container App.
|
|
108
|
+
*/
|
|
109
|
+
async function checkNetworkMetrics(
|
|
110
|
+
resource: armResources.GenericResource,
|
|
111
|
+
monitorClient: MonitorClient,
|
|
112
|
+
timespanDays: number,
|
|
113
|
+
reason: string,
|
|
114
|
+
verbose: boolean,
|
|
115
|
+
): Promise<string> {
|
|
116
|
+
let newReason = reason;
|
|
117
|
+
|
|
118
|
+
if (!resource.id) {
|
|
119
|
+
return newReason;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
verboseLog(verbose, "Checking network metrics...");
|
|
123
|
+
|
|
124
|
+
const networkIn = await getMetric(
|
|
125
|
+
monitorClient,
|
|
126
|
+
resource.id,
|
|
127
|
+
"RxBytes",
|
|
128
|
+
"Average",
|
|
129
|
+
timespanDays,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const networkOut = await getMetric(
|
|
133
|
+
monitorClient,
|
|
134
|
+
resource.id,
|
|
135
|
+
"TxBytes",
|
|
136
|
+
"Average",
|
|
137
|
+
timespanDays,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
verboseLog(
|
|
141
|
+
verbose,
|
|
142
|
+
`Network In: ${networkIn !== null ? `${(networkIn / 1048576).toFixed(2)} MB/day avg` : "N/A"}`,
|
|
143
|
+
);
|
|
144
|
+
verboseLog(
|
|
145
|
+
verbose,
|
|
146
|
+
`Network Out: ${networkOut !== null ? `${(networkOut / 1048576).toFixed(2)} MB/day avg` : "N/A"}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
networkIn !== null &&
|
|
151
|
+
networkOut !== null &&
|
|
152
|
+
networkIn + networkOut < 34000
|
|
153
|
+
) {
|
|
154
|
+
newReason += `Very low network traffic (${((networkIn + networkOut) / 1048576).toFixed(2)} MB/day avg). `;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return newReason;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Checks resource usage metrics for a Container App.
|
|
162
|
+
*/
|
|
163
|
+
async function checkResourceMetrics(
|
|
164
|
+
resource: armResources.GenericResource,
|
|
165
|
+
monitorClient: MonitorClient,
|
|
166
|
+
timespanDays: number,
|
|
167
|
+
reason: string,
|
|
168
|
+
verbose: boolean,
|
|
169
|
+
): Promise<string> {
|
|
170
|
+
let newReason = reason;
|
|
171
|
+
|
|
172
|
+
if (!resource.id) {
|
|
173
|
+
return newReason;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
verboseLog(verbose, "Checking resource usage metrics...");
|
|
177
|
+
|
|
178
|
+
const cpuUsage = await getMetric(
|
|
179
|
+
monitorClient,
|
|
180
|
+
resource.id,
|
|
181
|
+
"UsageNanoCores",
|
|
182
|
+
"Average",
|
|
183
|
+
timespanDays,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const memoryUsage = await getMetric(
|
|
187
|
+
monitorClient,
|
|
188
|
+
resource.id,
|
|
189
|
+
"WorkingSetBytes",
|
|
190
|
+
"Average",
|
|
191
|
+
timespanDays,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
verboseLog(
|
|
195
|
+
verbose,
|
|
196
|
+
`CPU Usage: ${cpuUsage !== null ? `${(cpuUsage / 1000000000).toFixed(4)} cores` : "N/A"}`,
|
|
197
|
+
);
|
|
198
|
+
verboseLog(
|
|
199
|
+
verbose,
|
|
200
|
+
`Memory Usage: ${memoryUsage !== null ? `${(memoryUsage / 1048576).toFixed(2)} MB` : "N/A"}`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (cpuUsage !== null && cpuUsage < 1000000) {
|
|
204
|
+
newReason += `Very low CPU usage (${(cpuUsage / 1000000000).toFixed(4)} cores). `;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (memoryUsage !== null && memoryUsage < 10485760) {
|
|
208
|
+
newReason += `Very low memory usage (${(memoryUsage / 1048576).toFixed(2)} MB). `;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return newReason;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Checks the running status of a Container App.
|
|
216
|
+
*/
|
|
217
|
+
function checkRunningStatus(
|
|
218
|
+
appDetails: {
|
|
219
|
+
properties?: {
|
|
220
|
+
provisioningState?: string;
|
|
221
|
+
runningStatus?: string;
|
|
222
|
+
template?: { scale?: { maxReplicas?: number; minReplicas?: number } };
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
reason: string,
|
|
226
|
+
verbose: boolean,
|
|
227
|
+
): string {
|
|
228
|
+
let newReason = reason;
|
|
229
|
+
|
|
230
|
+
const { provisioningState, runningStatus, template } =
|
|
231
|
+
appDetails.properties || {};
|
|
232
|
+
|
|
233
|
+
verboseLog(verbose, `Min Replicas: ${template?.scale?.minReplicas ?? "N/A"}`);
|
|
234
|
+
verboseLog(verbose, `Max Replicas: ${template?.scale?.maxReplicas ?? "N/A"}`);
|
|
235
|
+
|
|
236
|
+
if (provisioningState === "Succeeded" && runningStatus !== "Running") {
|
|
237
|
+
newReason += "Container App is not running. ";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const minReplicas = template?.scale?.minReplicas;
|
|
241
|
+
const maxReplicas = template?.scale?.maxReplicas;
|
|
242
|
+
if (minReplicas === 0 && maxReplicas === 0) {
|
|
243
|
+
newReason += "Container App has 0 replicas configured. ";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return newReason;
|
|
247
|
+
}
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { analyzeAppServicePlan } from "./app-service.js";
|
|
6
|
+
export { analyzeContainerApp } from "./container-app.js";
|
|
6
7
|
export { analyzeDisk } from "./disk.js";
|
|
7
8
|
export { analyzeNic } from "./nic.js";
|
|
8
9
|
export { analyzePrivateEndpoint } from "./private-endpoint.js";
|
|
9
10
|
export { analyzePublicIp } from "./public-ip.js";
|
|
11
|
+
export { analyzeStaticSite } from "./static-web-app.js";
|
|
10
12
|
export { analyzeStorageAccount } from "./storage.js";
|
|
11
13
|
export { analyzeVM } from "./vm.js";
|
|
@@ -83,13 +83,13 @@ export async function analyzePublicIp(
|
|
|
83
83
|
monitorClient,
|
|
84
84
|
resource.id,
|
|
85
85
|
"BytesInDDoS",
|
|
86
|
-
"
|
|
86
|
+
"Average",
|
|
87
87
|
timespanDays,
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
-
if (bytesInDDoS !== null && bytesInDDoS <
|
|
91
|
-
// Less than
|
|
92
|
-
reason += `Very low network traffic (${(bytesInDDoS / 1024 / 1024).toFixed(2)} MB). `;
|
|
90
|
+
if (bytesInDDoS !== null && bytesInDDoS < 340000) {
|
|
91
|
+
// Less than ~340KB average per day
|
|
92
|
+
reason += `Very low network traffic (${(bytesInDDoS / 1024 / 1024).toFixed(2)} MB/day avg). `;
|
|
93
93
|
}
|
|
94
94
|
} catch (error) {
|
|
95
95
|
const logger = getLogger(["savemoney", "azure"]);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Static Web App analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { MonitorClient } from "@azure/arm-monitor";
|
|
6
|
+
|
|
7
|
+
import * as armResources from "@azure/arm-resources";
|
|
8
|
+
import { getLogger } from "@logtape/logtape";
|
|
9
|
+
|
|
10
|
+
import type { AnalysisResult } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getMetric,
|
|
14
|
+
verboseLog,
|
|
15
|
+
verboseLogAnalysisResult,
|
|
16
|
+
verboseLogResourceStart,
|
|
17
|
+
} from "../utils.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Analyzes an Azure Static Web App for potential cost optimization.
|
|
21
|
+
*
|
|
22
|
+
* @param resource - The Azure resource object
|
|
23
|
+
* @param monitorClient - Azure Monitor client for metrics
|
|
24
|
+
* @param timespanDays - Number of days to analyze metrics
|
|
25
|
+
* @param verbose - Enable verbose logging
|
|
26
|
+
* @returns Analysis result with cost risk and reason
|
|
27
|
+
*/
|
|
28
|
+
export async function analyzeStaticSite(
|
|
29
|
+
resource: armResources.GenericResource,
|
|
30
|
+
monitorClient: MonitorClient,
|
|
31
|
+
timespanDays: number,
|
|
32
|
+
verbose = false,
|
|
33
|
+
): Promise<AnalysisResult> {
|
|
34
|
+
verboseLogResourceStart(
|
|
35
|
+
verbose,
|
|
36
|
+
resource.name || "unknown",
|
|
37
|
+
"Static Web App (Microsoft.Web/staticSites)",
|
|
38
|
+
);
|
|
39
|
+
verboseLog(verbose, "Resource details:", resource);
|
|
40
|
+
|
|
41
|
+
const costRisk: "high" | "low" | "medium" = "low";
|
|
42
|
+
let reason = "";
|
|
43
|
+
|
|
44
|
+
if (!resource.id) {
|
|
45
|
+
return {
|
|
46
|
+
costRisk,
|
|
47
|
+
reason: "Resource ID is missing.",
|
|
48
|
+
suspectedUnused: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
verboseLog(verbose, "Checking metrics...");
|
|
54
|
+
|
|
55
|
+
// Check for site hits (requests to the static site)
|
|
56
|
+
// Note: Static Web Apps metrics use Total aggregation, not Average
|
|
57
|
+
// Ref. https://learn.microsoft.com/en-us/azure/azure-monitor/reference/supported-metrics/microsoft-web-staticsites-metrics
|
|
58
|
+
const siteHits = await getMetric(
|
|
59
|
+
monitorClient,
|
|
60
|
+
resource.id,
|
|
61
|
+
"SiteHits",
|
|
62
|
+
"Total",
|
|
63
|
+
timespanDays,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const bytesSent = await getMetric(
|
|
67
|
+
monitorClient,
|
|
68
|
+
resource.id,
|
|
69
|
+
"BytesSent",
|
|
70
|
+
"Total",
|
|
71
|
+
timespanDays,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
verboseLog(
|
|
75
|
+
verbose,
|
|
76
|
+
`Site Hits: ${siteHits !== null ? `${siteHits.toFixed(0)} total requests` : "N/A"}`,
|
|
77
|
+
);
|
|
78
|
+
verboseLog(
|
|
79
|
+
verbose,
|
|
80
|
+
`Bytes Sent: ${bytesSent !== null ? `${(bytesSent / 1024 / 1024).toFixed(2)} MB total` : "N/A"}`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// If both metrics are null, it means no data points exist (no traffic at all)
|
|
84
|
+
if (siteHits === null && bytesSent === null) {
|
|
85
|
+
reason += `No traffic data available in ${timespanDays} days. `;
|
|
86
|
+
} else {
|
|
87
|
+
if (siteHits !== null && siteHits < 100) {
|
|
88
|
+
// Less than 100 requests total in the timespan (< ~3.3 requests/day)
|
|
89
|
+
reason += `Very low site traffic (${siteHits.toFixed(0)} requests in ${timespanDays} days). `;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (bytesSent !== null && bytesSent < 1048576) {
|
|
93
|
+
// Less than 1MB total in the timespan (< ~34KB/day)
|
|
94
|
+
reason += `Very low data transfer (${(bytesSent / 1024 / 1024).toFixed(2)} MB in ${timespanDays} days). `;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
99
|
+
logger.warn(
|
|
100
|
+
`Failed to get metrics for Static Web App ${resource.name}: ${error instanceof Error ? error.message : error}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const suspectedUnused = reason.length > 0;
|
|
105
|
+
const result = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
106
|
+
verboseLogAnalysisResult(verbose, result);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
@@ -48,14 +48,14 @@ export async function analyzeStorageAccount(
|
|
|
48
48
|
monitorClient,
|
|
49
49
|
resource.id,
|
|
50
50
|
"Transactions",
|
|
51
|
-
"
|
|
51
|
+
"Average",
|
|
52
52
|
timespanDays,
|
|
53
53
|
);
|
|
54
|
-
if (transactions !== null && transactions <
|
|
55
|
-
//
|
|
54
|
+
if (transactions !== null && transactions < 10) {
|
|
55
|
+
// Less than 10 transactions per day on average
|
|
56
56
|
const result = {
|
|
57
57
|
costRisk,
|
|
58
|
-
reason: `Very low transaction count (${transactions}). `,
|
|
58
|
+
reason: `Very low transaction count (${transactions.toFixed(2)} avg/day). `,
|
|
59
59
|
suspectedUnused: true,
|
|
60
60
|
};
|
|
61
61
|
verboseLogAnalysisResult(verbose, result);
|