gitgreen 0.1.1 → 1.0.1
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/LICENSE +21 -0
- package/README.md +213 -49
- package/dist/cli.js +204 -28
- package/dist/init.js +582 -40
- package/dist/lib/aws/cloudwatch.js +110 -0
- package/dist/lib/carbon/carbon-calculator.js +9 -7
- package/dist/lib/gitlab/report-formatter.js +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchCloudWatchTimeseries = void 0;
|
|
4
|
+
const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
|
|
5
|
+
const toTimeseries = (result) => {
|
|
6
|
+
if (!result)
|
|
7
|
+
return [];
|
|
8
|
+
const timestamps = result.Timestamps || [];
|
|
9
|
+
const values = result.Values || [];
|
|
10
|
+
const points = [];
|
|
11
|
+
const len = Math.min(timestamps.length, values.length);
|
|
12
|
+
for (let i = 0; i < len; i++) {
|
|
13
|
+
const value = values[i];
|
|
14
|
+
if (value === undefined || value === null)
|
|
15
|
+
continue;
|
|
16
|
+
const ts = timestamps[i];
|
|
17
|
+
points.push({
|
|
18
|
+
timestamp: ts instanceof Date ? ts.toISOString() : new Date(ts).toISOString(),
|
|
19
|
+
value: Number(value)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return points.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
23
|
+
};
|
|
24
|
+
const fetchCloudWatchTimeseries = async (opts) => {
|
|
25
|
+
const cw = new client_cloudwatch_1.CloudWatchClient({ region: opts.region });
|
|
26
|
+
const periodSeconds = opts.periodSeconds ?? 60;
|
|
27
|
+
const command = new client_cloudwatch_1.GetMetricDataCommand({
|
|
28
|
+
StartTime: opts.startTime,
|
|
29
|
+
EndTime: opts.endTime,
|
|
30
|
+
ScanBy: 'TimestampAscending',
|
|
31
|
+
MetricDataQueries: [
|
|
32
|
+
{
|
|
33
|
+
Id: 'cpu',
|
|
34
|
+
MetricStat: {
|
|
35
|
+
Metric: {
|
|
36
|
+
Namespace: 'AWS/EC2',
|
|
37
|
+
MetricName: 'CPUUtilization',
|
|
38
|
+
Dimensions: [{ Name: 'InstanceId', Value: opts.instanceId }]
|
|
39
|
+
},
|
|
40
|
+
Period: periodSeconds,
|
|
41
|
+
Stat: 'Average'
|
|
42
|
+
},
|
|
43
|
+
ReturnData: true
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
Id: 'ramUsed',
|
|
47
|
+
MetricStat: {
|
|
48
|
+
Metric: {
|
|
49
|
+
Namespace: 'CWAgent',
|
|
50
|
+
MetricName: 'mem_used',
|
|
51
|
+
Dimensions: [{ Name: 'InstanceId', Value: opts.instanceId }]
|
|
52
|
+
},
|
|
53
|
+
Period: periodSeconds,
|
|
54
|
+
Stat: 'Average'
|
|
55
|
+
},
|
|
56
|
+
ReturnData: true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
Id: 'ramTotal',
|
|
60
|
+
MetricStat: {
|
|
61
|
+
Metric: {
|
|
62
|
+
Namespace: 'CWAgent',
|
|
63
|
+
MetricName: 'mem_total',
|
|
64
|
+
Dimensions: [{ Name: 'InstanceId', Value: opts.instanceId }]
|
|
65
|
+
},
|
|
66
|
+
Period: periodSeconds,
|
|
67
|
+
Stat: 'Average'
|
|
68
|
+
},
|
|
69
|
+
ReturnData: true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
Id: 'ramUsedPercent',
|
|
73
|
+
MetricStat: {
|
|
74
|
+
Metric: {
|
|
75
|
+
Namespace: 'CWAgent',
|
|
76
|
+
MetricName: 'mem_used_percent',
|
|
77
|
+
Dimensions: [{ Name: 'InstanceId', Value: opts.instanceId }]
|
|
78
|
+
},
|
|
79
|
+
Period: periodSeconds,
|
|
80
|
+
Stat: 'Average'
|
|
81
|
+
},
|
|
82
|
+
ReturnData: true
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
const resp = await cw.send(command);
|
|
87
|
+
const byId = new Map();
|
|
88
|
+
(resp.MetricDataResults || []).forEach(result => {
|
|
89
|
+
if (result.Id)
|
|
90
|
+
byId.set(result.Id, result);
|
|
91
|
+
});
|
|
92
|
+
const cpuUtilization = toTimeseries(byId.get('cpu'));
|
|
93
|
+
const ramUsed = toTimeseries(byId.get('ramUsed'));
|
|
94
|
+
let ramTotal = toTimeseries(byId.get('ramTotal'));
|
|
95
|
+
const ramUsedPercent = toTimeseries(byId.get('ramUsedPercent'));
|
|
96
|
+
let ramUsedSeries = ramUsed;
|
|
97
|
+
const fallbackTotal = ramTotal[0]?.value || opts.memoryBytesFallback;
|
|
98
|
+
if (!ramUsedSeries.length && ramUsedPercent.length && fallbackTotal) {
|
|
99
|
+
ramUsedSeries = ramUsedPercent.map(point => ({
|
|
100
|
+
timestamp: point.timestamp,
|
|
101
|
+
value: (point.value / 100) * fallbackTotal
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
if (!ramTotal.length && fallbackTotal) {
|
|
105
|
+
const ts = ramUsedSeries[0]?.timestamp || new Date().toISOString();
|
|
106
|
+
ramTotal = [{ timestamp: ts, value: fallbackTotal }];
|
|
107
|
+
}
|
|
108
|
+
return { cpuUtilization, ramUsed: ramUsedSeries, ramTotal };
|
|
109
|
+
};
|
|
110
|
+
exports.fetchCloudWatchTimeseries = fetchCloudWatchTimeseries;
|
|
@@ -36,17 +36,19 @@ class CarbonCalculator {
|
|
|
36
36
|
const cpuSorted = [...job.cpuTimeseries].sort((a, b) => this.parseTimestamp(a.timestamp) - this.parseTimestamp(b.timestamp));
|
|
37
37
|
const ramUsedSorted = [...job.ramUsedTimeseries].sort((a, b) => this.parseTimestamp(a.timestamp) - this.parseTimestamp(b.timestamp));
|
|
38
38
|
const ramSizeSorted = [...job.ramSizeTimeseries].sort((a, b) => this.parseTimestamp(a.timestamp) - this.parseTimestamp(b.timestamp));
|
|
39
|
-
|
|
40
|
-
const GCP_INTERVAL_SECONDS = 60;
|
|
39
|
+
const DEFAULT_INTERVAL_SECONDS = 60;
|
|
41
40
|
// Integrate CPU energy over timeseries
|
|
42
41
|
let cpuEnergyKwh = 0;
|
|
43
42
|
for (let i = 0; i < cpuSorted.length; i++) {
|
|
44
|
-
const
|
|
43
|
+
const rawCpuValue = cpuSorted[i].value;
|
|
44
|
+
const cpuPercent = job.provider === 'aws'
|
|
45
|
+
? (rawCpuValue <= 1 ? rawCpuValue * 100 : rawCpuValue)
|
|
46
|
+
: rawCpuValue * 100;
|
|
45
47
|
const powerWatts = this.interpolatePower(cpuProfile, cpuPercent);
|
|
46
48
|
// Calculate interval from timestamps, or use GCP interval for single point
|
|
47
49
|
let intervalSeconds;
|
|
48
50
|
if (cpuSorted.length === 1) {
|
|
49
|
-
intervalSeconds =
|
|
51
|
+
intervalSeconds = DEFAULT_INTERVAL_SECONDS;
|
|
50
52
|
}
|
|
51
53
|
else if (i < cpuSorted.length - 1) {
|
|
52
54
|
intervalSeconds = this.parseTimestamp(cpuSorted[i + 1].timestamp) - this.parseTimestamp(cpuSorted[i].timestamp);
|
|
@@ -65,7 +67,7 @@ class CarbonCalculator {
|
|
|
65
67
|
const powerWatts = ramUsedGb * RAM_WATTS_PER_GB;
|
|
66
68
|
let intervalSeconds;
|
|
67
69
|
if (ramUsedSorted.length === 1) {
|
|
68
|
-
intervalSeconds =
|
|
70
|
+
intervalSeconds = DEFAULT_INTERVAL_SECONDS;
|
|
69
71
|
}
|
|
70
72
|
else if (i < ramUsedSorted.length - 1) {
|
|
71
73
|
intervalSeconds = this.parseTimestamp(ramUsedSorted[i + 1].timestamp) - this.parseTimestamp(ramUsedSorted[i].timestamp);
|
|
@@ -78,8 +80,8 @@ class CarbonCalculator {
|
|
|
78
80
|
// Calculate total runtime from timeseries
|
|
79
81
|
const firstTs = Math.min(cpuSorted.length > 0 ? this.parseTimestamp(cpuSorted[0].timestamp) : Infinity, ramUsedSorted.length > 0 ? this.parseTimestamp(ramUsedSorted[0].timestamp) : Infinity);
|
|
80
82
|
const lastTs = Math.max(cpuSorted.length > 0 ? this.parseTimestamp(cpuSorted[cpuSorted.length - 1].timestamp) : 0, ramUsedSorted.length > 0 ? this.parseTimestamp(ramUsedSorted[ramUsedSorted.length - 1].timestamp) : 0);
|
|
81
|
-
// If single point, use
|
|
82
|
-
const runtimeSeconds = lastTs === firstTs ?
|
|
83
|
+
// If single point, use default interval; otherwise use actual span
|
|
84
|
+
const runtimeSeconds = lastTs === firstTs ? DEFAULT_INTERVAL_SECONDS : (lastTs - firstTs);
|
|
83
85
|
const runtimeHours = runtimeSeconds / 3600;
|
|
84
86
|
// Calculate emissions
|
|
85
87
|
const cpuEmissions = cpuEnergyKwh * pue * carbonIntensity;
|
|
@@ -64,7 +64,12 @@ const buildMarkdownReport = (result, job, budget) => {
|
|
|
64
64
|
}
|
|
65
65
|
// Add CPU chart (only if more than 1 point)
|
|
66
66
|
if (cpuSorted.length > 1) {
|
|
67
|
-
const cpuValues = cpuSorted.map(p =>
|
|
67
|
+
const cpuValues = cpuSorted.map(p => {
|
|
68
|
+
if (job.provider === 'aws') {
|
|
69
|
+
return p.value <= 1 ? p.value * 100 : p.value;
|
|
70
|
+
}
|
|
71
|
+
return p.value * 100;
|
|
72
|
+
});
|
|
68
73
|
lines.push('', '## CPU Utilization (%)', '```', asciichart_1.default.plot(cpuValues, { height: 6 }), '```');
|
|
69
74
|
}
|
|
70
75
|
// Add RAM chart (only if more than 1 point)
|