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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 GitGreen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,77 +1,241 @@
|
|
|
1
1
|
# GitGreen CLI
|
|
2
2
|
|
|
3
|
-
Self-contained carbon calculation CLI for GitLab jobs (no API server). It reuses the same power profiles and budget reporting as the existing implementation, pulls Electricity Maps intensity, and can post Merge Request notes via `CI_JOB_TOKEN`.
|
|
3
|
+
Self-contained carbon calculation CLI for GitLab jobs (no API server). It reuses the same power profiles and budget reporting as the existing implementation, pulls Electricity Maps intensity, and can post Merge Request notes via `CI_JOB_TOKEN`. Works with both GCP and AWS runners.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Before installing GitGreen CLI, ensure you have the following installed and configured:
|
|
8
|
+
|
|
9
|
+
### Required
|
|
10
|
+
|
|
11
|
+
- **Node.js** (v20 or higher) - [Download](https://nodejs.org/)
|
|
12
|
+
- Required to run the CLI tool
|
|
13
|
+
- Check version: `node --version`
|
|
14
|
+
|
|
15
|
+
- **npm** or **pnpm** - Package manager (comes with Node.js)
|
|
16
|
+
- This project uses `pnpm@8.15.4` as the package manager
|
|
17
|
+
- Install pnpm: `npm install -g pnpm`
|
|
18
|
+
|
|
19
|
+
- **Git** - [Download](https://git-scm.com/)
|
|
20
|
+
- Required for detecting GitLab project information during initialization
|
|
21
|
+
- Check version: `git --version`
|
|
22
|
+
|
|
23
|
+
### Cloud Provider CLIs (Required based on provider)
|
|
24
|
+
|
|
25
|
+
- **Google Cloud SDK (gcloud CLI)** - [Installation Guide](https://cloud.google.com/sdk/docs/install-sdk)
|
|
26
|
+
- Required for GCP provider setup and authentication
|
|
27
|
+
- After installation, authenticate: `gcloud auth login`
|
|
28
|
+
- Set default project: `gcloud config set project YOUR_PROJECT_ID`
|
|
29
|
+
- Check version: `gcloud --version`
|
|
30
|
+
|
|
31
|
+
- **AWS Credentials** - [AWS CLI Installation](https://aws.amazon.com/cli/) (optional, but recommended)
|
|
32
|
+
- Required for AWS provider (can use environment variables or AWS CLI config)
|
|
33
|
+
- Configure credentials: `aws configure`
|
|
34
|
+
- Or set environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`
|
|
35
|
+
|
|
36
|
+
### Optional (but recommended)
|
|
37
|
+
|
|
38
|
+
- **GitLab CLI (glab)** - [Installation Guide](https://docs.gitlab.com/cli/)
|
|
39
|
+
- Recommended for easier GitLab authentication and CI/CD variable management
|
|
40
|
+
- After installation, authenticate: `glab auth login`
|
|
41
|
+
- Check version: `glab --version`
|
|
42
|
+
- If not installed, you'll need to provide a GitLab Personal Access Token manually
|
|
43
|
+
|
|
44
|
+
### API Keys
|
|
45
|
+
|
|
46
|
+
- **Electricity Maps API Key** - [Get free key](https://api-portal.electricitymaps.com)
|
|
47
|
+
- Required for carbon intensity data
|
|
48
|
+
- You'll be prompted for this during `gitgreen init`
|
|
4
49
|
|
|
5
50
|
## Install
|
|
6
51
|
- From npm (global CLI):
|
|
7
52
|
```bash
|
|
8
|
-
|
|
9
|
-
# or: npm install -g gitgreen-cli
|
|
53
|
+
npm install -g gitgreen
|
|
10
54
|
gitgreen --help
|
|
11
55
|
```
|
|
12
56
|
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
# from repo root
|
|
16
|
-
pnpm -C node-module install
|
|
17
|
-
# build happens via prepare; dist/ is ready for CI
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Run tests (pnpm preferred):
|
|
57
|
+
Run tests:
|
|
21
58
|
```bash
|
|
22
|
-
|
|
23
|
-
# or: npm --prefix node-module test
|
|
59
|
+
npm test
|
|
24
60
|
```
|
|
25
61
|
|
|
26
62
|
Stress test multiple live configs (build first, real APIs):
|
|
27
63
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
64
|
+
npm build
|
|
65
|
+
npm stress
|
|
30
66
|
```
|
|
31
67
|
|
|
32
68
|
## Usage
|
|
33
|
-
```bash
|
|
34
|
-
gitgreen \
|
|
35
|
-
--provider gcp \
|
|
36
|
-
--machine e2-standard-4 \
|
|
37
|
-
--region us-central1-a \
|
|
38
|
-
--duration 1800 \
|
|
39
|
-
--cpu 55 \
|
|
40
|
-
--budget 500 \
|
|
41
|
-
--fail-on-budget \
|
|
42
|
-
--out-md carbon-report.md \
|
|
43
|
-
--out-json carbon-report.json \
|
|
44
|
-
--post-note
|
|
45
|
-
```
|
|
46
69
|
|
|
47
|
-
###
|
|
70
|
+
### Initializing a New GitLab Project
|
|
71
|
+
|
|
72
|
+
To get started with carbon tracking in any GitLab project (any project with a remote pointing to a GitLab instance), run:
|
|
73
|
+
|
|
48
74
|
```bash
|
|
49
75
|
# In your repo
|
|
50
76
|
gitgreen init
|
|
51
77
|
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
|
|
79
|
+
The initialization wizard will guide you through the setup process:
|
|
80
|
+
- Configure provider/machine/region settings
|
|
81
|
+
- Set carbon budgets
|
|
82
|
+
- Configure MR note preferences
|
|
83
|
+
- Choose to use an existing runner or spin up a new one with supported providers
|
|
84
|
+
|
|
85
|
+
After initialization, the wizard will:
|
|
86
|
+
- Append a ready-made job to your `.gitlab-ci.yml`
|
|
87
|
+
- Print the CI/CD variable checklist (ELECTRICITY_MAPS_API_KEY, provider credentials, budget flags)
|
|
88
|
+
|
|
89
|
+
### How It Works
|
|
90
|
+
|
|
91
|
+
Once initialized, all subsequent pipelines will run on the configured runner, and their performance will be automatically measured. The carbon tracking is implemented using GitLab CI/CD components as a final step in your pipeline. The carbon tracking job itself is not computationally expensive, so it adds minimal overhead to your CI/CD workflows.
|
|
92
|
+
|
|
93
|
+
Key environment variables:
|
|
94
|
+
- `ELECTRICITY_MAPS_API_KEY` (required) - Get a free key from https://api-portal.electricitymaps.com
|
|
95
|
+
- `GCP_PROJECT_ID` / `GOOGLE_CLOUD_PROJECT` (required for GCP) - Your GCP project ID
|
|
96
|
+
- `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (required for AWS CloudWatch) - Credentials with CloudWatch read access on the runner
|
|
97
|
+
- `AWS_REGION` (for AWS) - Region of the runner EC2 instance
|
|
98
|
+
|
|
99
|
+
The `gitgreen init` wizard will automatically set all necessary GitLab CI/CD variables for you.
|
|
100
|
+
|
|
101
|
+
## Providers
|
|
102
|
+
|
|
103
|
+
GitGreen supports multiple cloud providers:
|
|
104
|
+
|
|
105
|
+
- **GCP**: Fully wired — the CLI parser expects GCP Monitoring JSON, the GitLab component polls GCE metrics, and `gitgreen init` provisions or reuses GCP runners.
|
|
106
|
+
- **AWS**: CloudWatch-backed — the CLI can pull metrics directly via `--from-cloudwatch`, the GitLab component fetches CloudWatch CPU/RAM data, and `gitgreen init` can configure or provision EC2 runners.
|
|
107
|
+
|
|
108
|
+
## Architecture
|
|
109
|
+
Runtime flow:
|
|
110
|
+
```
|
|
111
|
+
CPU/RAM timeseries (GCP Monitoring JSON or custom collector)
|
|
112
|
+
|
|
|
113
|
+
v
|
|
114
|
+
+------------------+ +----------------------------+
|
|
115
|
+
| CLI (gitgreen) |--->| CarbonCalculator |
|
|
116
|
+
| - parse metrics | | - PowerProfileRepository |<- machine data in data/*.json
|
|
117
|
+
| - CLI/env opts | | - ZoneMapper (region→zone) |<- static + runtime PUE mapping
|
|
118
|
+
+------------------+ | - IntensityProvider |<- Electricity Maps API
|
|
119
|
+
+-------------+------------+
|
|
120
|
+
|
|
|
121
|
+
v
|
|
122
|
+
+-----------------------------+
|
|
123
|
+
| Report formatter |
|
|
124
|
+
| - Markdown/JSON artifacts |
|
|
125
|
+
| - Budget evaluation |
|
|
126
|
+
+-------------+---------------+
|
|
127
|
+
|
|
|
128
|
+
v
|
|
129
|
+
+-----------------------------+
|
|
130
|
+
| GitLab client (optional) |
|
|
131
|
+
| - MR note via CI_JOB_TOKEN |
|
|
132
|
+
+-----------------------------+
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
GitLab CI path:
|
|
136
|
+
```
|
|
137
|
+
Pipeline starts → component script fetches CPU/RAM timeseries from GCP Monitoring or AWS CloudWatch
|
|
138
|
+
→ writes JSON files → runs `gitgreen --provider gcp|aws ...`
|
|
139
|
+
→ emits `carbon-report.md` / `carbon-report.json`
|
|
140
|
+
→ optional MR note when CI_JOB_TOKEN is present
|
|
71
141
|
```
|
|
72
|
-
|
|
142
|
+
|
|
143
|
+
## Adding a provider
|
|
144
|
+
1. Extend `CloudProvider` and the provider guard in `src/index.ts` so the calculator accepts the new key.
|
|
145
|
+
2. Add machine power data (`<provider>_machine_power_profiles.json`) and, if needed, CPU profiles to `data/`, then update `PowerProfileRepository.loadMachineData` to load it.
|
|
146
|
+
3. Map regions to Electricity Maps zones and a PUE default in `ZoneMapper` (or via `data/runtime-pue-mappings.json` for runtime overrides).
|
|
147
|
+
4. Parse that provider's metrics into the `TimeseriesPoint` shape (timestamp + numeric value) alongside RAM size/usage, and update the CLI/init/templates to pull those metrics.
|
|
148
|
+
5. Wire any CI automation (runner tags, MR note flags) to pass the correct provider, machine type, and region strings.
|
|
73
149
|
|
|
74
150
|
## Publish
|
|
75
151
|
- Ensure version bump in `package.json`
|
|
76
152
|
- Run `pnpm -C node-module build && pnpm -C node-module test`
|
|
77
153
|
- Publish: `pnpm -C node-module publish --access public`
|
|
154
|
+
|
|
155
|
+
## FAQ
|
|
156
|
+
|
|
157
|
+
### Are my API keys and credentials secure?
|
|
158
|
+
|
|
159
|
+
Yes. GitGreen does not save your keys locally. During `gitgreen init`, all sensitive credentials (API keys, service account keys, AWS credentials) are stored only in GitLab CI/CD variables, which are encrypted and managed by GitLab. The CLI never writes credentials to disk on your local machine.
|
|
160
|
+
|
|
161
|
+
### Does GitGreen call any third-party APIs?
|
|
162
|
+
|
|
163
|
+
GitGreen only calls APIs that you explicitly configure and authorize:
|
|
164
|
+
|
|
165
|
+
- **Electricity Maps API** - For carbon intensity data (requires your API key)
|
|
166
|
+
- **GitLab API** - Only when using `glab` CLI or providing a PAT, to set CI/CD variables
|
|
167
|
+
- **GCP Monitoring API** - To fetch CPU/RAM metrics from your GCP project (requires your service account)
|
|
168
|
+
- **AWS CloudWatch API** - To fetch CPU/RAM metrics from your AWS account (requires your AWS credentials)
|
|
169
|
+
|
|
170
|
+
GitGreen does not operate any backend services or call any APIs owned by the GitGreen project. All API calls are made directly from your environment or CI/CD pipeline using your credentials.
|
|
171
|
+
|
|
172
|
+
### How do I know GitGreen won't destroy my `.gitlab-ci.yml` file?
|
|
173
|
+
|
|
174
|
+
GitGreen takes safety precautions when modifying your `.gitlab-ci.yml`:
|
|
175
|
+
|
|
176
|
+
1. **Automatic backups** - Before making any changes, GitGreen creates a backup of your existing `.gitlab-ci.yml` file
|
|
177
|
+
2. **Backup location** - The backup path is printed to the console (e.g., `/tmp/gitlab-ci-backup-{timestamp}.yml`)
|
|
178
|
+
3. **Append-only changes** - GitGreen only appends new content to your CI file; it never removes or modifies existing jobs
|
|
179
|
+
4. **Confirmation prompt** - You're asked to confirm before any changes are made
|
|
180
|
+
|
|
181
|
+
If something goes wrong, you can restore your file from the backup location that was printed.
|
|
182
|
+
|
|
183
|
+
### Can I use GitGreen without the `init` wizard?
|
|
184
|
+
|
|
185
|
+
Yes. You can configure GitGreen manually by:
|
|
186
|
+
|
|
187
|
+
1. Setting the required CI/CD variables in your GitLab project settings
|
|
188
|
+
2. Adding the GitLab CI/CD component to your `.gitlab-ci.yml` manually
|
|
189
|
+
3. Running `gitgreen` directly with command-line options instead of using the component
|
|
190
|
+
|
|
191
|
+
The `init` wizard is a convenience tool, but all functionality is available through manual configuration.
|
|
192
|
+
|
|
193
|
+
### What data does GitGreen collect?
|
|
194
|
+
|
|
195
|
+
GitGreen only collects:
|
|
196
|
+
|
|
197
|
+
- **CPU and RAM usage metrics** from your cloud provider (GCP Monitoring or AWS CloudWatch)
|
|
198
|
+
- **Pipeline metadata** (start/end times) from GitLab CI/CD environment variables
|
|
199
|
+
|
|
200
|
+
GitGreen does not collect any personal information, code, or data from your repositories. All calculations happen locally in your CI/CD pipeline.
|
|
201
|
+
|
|
202
|
+
### Can I run GitGreen locally for testing?
|
|
203
|
+
|
|
204
|
+
Yes. You can run `gitgreen` directly with your own metrics files:
|
|
205
|
+
|
|
206
|
+
**GCP example:**
|
|
207
|
+
```bash
|
|
208
|
+
gitgreen --provider gcp \
|
|
209
|
+
--machine e2-standard-4 \
|
|
210
|
+
--region us-central1-a \
|
|
211
|
+
--cpu-timeseries cpu.json \
|
|
212
|
+
--ram-used-timeseries ram-used.json \
|
|
213
|
+
--ram-size-timeseries ram-size.json \
|
|
214
|
+
--out-md report.md
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**AWS example:**
|
|
218
|
+
```bash
|
|
219
|
+
gitgreen --provider aws \
|
|
220
|
+
--machine t3.medium \
|
|
221
|
+
--region us-east-1 \
|
|
222
|
+
--cpu-timeseries cpu.json \
|
|
223
|
+
--ram-used-timeseries ram-used.json \
|
|
224
|
+
--ram-size-timeseries ram-size.json \
|
|
225
|
+
--out-md report.md
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Or use `--from-cloudwatch` to fetch metrics directly from AWS CloudWatch:
|
|
229
|
+
```bash
|
|
230
|
+
gitgreen --provider aws \
|
|
231
|
+
--machine t3.medium \
|
|
232
|
+
--region us-east-1 \
|
|
233
|
+
--from-cloudwatch \
|
|
234
|
+
--out-md report.md
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This is useful for testing before integrating into your CI/CD pipeline.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
package/dist/cli.js
CHANGED
|
@@ -8,47 +8,217 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const commander_1 = require("commander");
|
|
9
9
|
const kleur_1 = require("kleur");
|
|
10
10
|
const asciichart_1 = __importDefault(require("asciichart"));
|
|
11
|
+
const config_1 = require("./config");
|
|
11
12
|
const index_1 = require("./index");
|
|
12
13
|
const init_1 = require("./init");
|
|
14
|
+
const cloudwatch_1 = require("./lib/aws/cloudwatch");
|
|
15
|
+
const power_profile_repository_1 = require("./lib/carbon/power-profile-repository");
|
|
13
16
|
const program = new commander_1.Command();
|
|
14
|
-
|
|
17
|
+
const toIsoTimestamp = (input) => {
|
|
18
|
+
if (input instanceof Date) {
|
|
19
|
+
return Number.isNaN(input.getTime()) ? undefined : input.toISOString();
|
|
20
|
+
}
|
|
21
|
+
if (typeof input === 'string' || typeof input === 'number') {
|
|
22
|
+
const date = new Date(input);
|
|
23
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
};
|
|
27
|
+
const toNumericValue = (input) => {
|
|
28
|
+
if (typeof input === 'number') {
|
|
29
|
+
return Number.isFinite(input) ? input : undefined;
|
|
30
|
+
}
|
|
31
|
+
if (typeof input === 'string') {
|
|
32
|
+
const parsed = Number(input);
|
|
33
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
};
|
|
37
|
+
const formatErrorMessage = (value) => {
|
|
38
|
+
if (value instanceof Error)
|
|
39
|
+
return value.message;
|
|
40
|
+
if (value === null || value === undefined)
|
|
41
|
+
return 'Unknown error';
|
|
42
|
+
return String(value);
|
|
43
|
+
};
|
|
44
|
+
const parseGcpTimeseries = (raw) => {
|
|
45
|
+
const points = [];
|
|
46
|
+
const response = raw;
|
|
47
|
+
const timeSeries = response.timeSeries;
|
|
48
|
+
if (!Array.isArray(timeSeries))
|
|
49
|
+
return points;
|
|
50
|
+
for (const ts of timeSeries) {
|
|
51
|
+
const series = ts;
|
|
52
|
+
if (!Array.isArray(series.points))
|
|
53
|
+
continue;
|
|
54
|
+
for (const point of series.points) {
|
|
55
|
+
const timestamp = toIsoTimestamp(point.interval?.endTime || point.interval?.startTime);
|
|
56
|
+
const value = point.value?.doubleValue !== undefined
|
|
57
|
+
? toNumericValue(point.value.doubleValue)
|
|
58
|
+
: toNumericValue(point.value?.int64Value);
|
|
59
|
+
if (timestamp && value !== undefined)
|
|
60
|
+
points.push({ timestamp, value });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return points;
|
|
64
|
+
};
|
|
65
|
+
const parseCloudWatchMetricData = (raw) => {
|
|
66
|
+
const data = raw.MetricDataResults || raw.metricDataResults;
|
|
67
|
+
const results = Array.isArray(data) ? data : [];
|
|
68
|
+
if (!results.length)
|
|
69
|
+
return [];
|
|
70
|
+
const first = results[0];
|
|
71
|
+
const timestamps = Array.isArray(first?.Timestamps) ? first.Timestamps : [];
|
|
72
|
+
const values = Array.isArray(first?.Values) ? first.Values : [];
|
|
73
|
+
const len = Math.min(timestamps.length, values.length);
|
|
74
|
+
const points = [];
|
|
75
|
+
for (let i = 0; i < len; i++) {
|
|
76
|
+
const timestamp = toIsoTimestamp(timestamps[i]);
|
|
77
|
+
const value = toNumericValue(values[i]);
|
|
78
|
+
if (timestamp && value !== undefined) {
|
|
79
|
+
points.push({ timestamp, value });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return points;
|
|
83
|
+
};
|
|
84
|
+
const parseTimeseriesFile = (filePath) => {
|
|
15
85
|
const raw = JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
|
|
86
|
+
const gcp = parseGcpTimeseries(raw);
|
|
87
|
+
if (gcp.length)
|
|
88
|
+
return gcp;
|
|
89
|
+
const cw = parseCloudWatchMetricData(raw);
|
|
90
|
+
if (cw.length)
|
|
91
|
+
return cw;
|
|
92
|
+
if (Array.isArray(raw)) {
|
|
93
|
+
const cwFromArray = parseCloudWatchMetricData({ MetricDataResults: raw });
|
|
94
|
+
if (cwFromArray.length)
|
|
95
|
+
return cwFromArray;
|
|
96
|
+
const legacy = parseLegacyTimeseries(raw);
|
|
97
|
+
if (legacy.length)
|
|
98
|
+
return legacy;
|
|
99
|
+
}
|
|
100
|
+
const datapointResponse = raw;
|
|
101
|
+
if (Array.isArray(datapointResponse.Datapoints)) {
|
|
102
|
+
const datapointSeries = convertCloudWatchDatapoints(datapointResponse.Datapoints);
|
|
103
|
+
if (datapointSeries.length)
|
|
104
|
+
return datapointSeries;
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
};
|
|
108
|
+
const parseLegacyTimeseries = (entries) => {
|
|
16
109
|
const points = [];
|
|
17
|
-
for (const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (p.value?.doubleValue !== undefined) {
|
|
22
|
-
value = p.value.doubleValue;
|
|
23
|
-
}
|
|
24
|
-
else if (p.value?.int64Value !== undefined) {
|
|
25
|
-
value = Number(p.value.int64Value);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const timestamp = toIsoTimestamp((entry.timestamp ?? entry.Timestamp));
|
|
112
|
+
const value = toNumericValue((entry.value ?? entry.Value));
|
|
113
|
+
if (timestamp && value !== undefined) {
|
|
30
114
|
points.push({ timestamp, value });
|
|
31
115
|
}
|
|
32
116
|
}
|
|
33
117
|
return points;
|
|
34
|
-
}
|
|
118
|
+
};
|
|
119
|
+
const convertCloudWatchDatapoints = (entries) => {
|
|
120
|
+
const points = [];
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const timestamp = toIsoTimestamp(entry.Timestamp);
|
|
123
|
+
const value = toNumericValue((entry.Average ?? entry.Maximum ?? entry.Minimum ?? entry.Sum));
|
|
124
|
+
if (timestamp && value !== undefined) {
|
|
125
|
+
points.push({ timestamp, value });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return points;
|
|
129
|
+
};
|
|
35
130
|
const runCalculate = async (opts) => {
|
|
36
131
|
const gitlabDisabled = opts.gitlab === false;
|
|
37
|
-
|
|
38
|
-
|
|
132
|
+
const provider = (opts.provider || config_1.config.defaultProvider);
|
|
133
|
+
const regionInput = opts.region || opts.awsRegion;
|
|
134
|
+
const awsEnvRegion = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
|
|
135
|
+
const calculationRegion = provider === 'aws' ? (regionInput || awsEnvRegion) : regionInput;
|
|
136
|
+
if (!opts.machine) {
|
|
137
|
+
console.error((0, kleur_1.red)('Error: --machine is required'));
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!calculationRegion) {
|
|
142
|
+
console.error((0, kleur_1.red)('Error: --region (or --aws-region) is required'));
|
|
39
143
|
process.exitCode = 1;
|
|
40
144
|
return;
|
|
41
145
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
146
|
+
let cpuTimeseries = [];
|
|
147
|
+
let ramUsedTimeseries = [];
|
|
148
|
+
let ramSizeTimeseries = [];
|
|
149
|
+
const timeseriesProvided = Boolean(opts.cpuTimeseries && opts.ramUsedTimeseries && opts.ramSizeTimeseries);
|
|
150
|
+
const shouldFetchCloudWatch = provider === 'aws' && (opts.fromCloudwatch || (!timeseriesProvided && opts.awsInstanceId));
|
|
151
|
+
if (shouldFetchCloudWatch) {
|
|
152
|
+
const instanceId = opts.awsInstanceId;
|
|
153
|
+
const resolvedRegion = calculationRegion || awsEnvRegion;
|
|
154
|
+
if (!resolvedRegion || !instanceId) {
|
|
155
|
+
console.error((0, kleur_1.red)('Error: --aws-instance-id and --region/--aws-region are required for AWS provider'));
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const awsInstanceId = instanceId;
|
|
160
|
+
const awsRegionValue = resolvedRegion;
|
|
161
|
+
const startTime = opts.awsStartTime
|
|
162
|
+
? new Date(opts.awsStartTime)
|
|
163
|
+
: process.env.CI_PIPELINE_CREATED_AT
|
|
164
|
+
? new Date(process.env.CI_PIPELINE_CREATED_AT)
|
|
165
|
+
: new Date(Date.now() - 60 * 60 * 1000);
|
|
166
|
+
const endTime = opts.awsEndTime ? new Date(opts.awsEndTime) : new Date();
|
|
167
|
+
const periodSeconds = opts.awsPeriod ? Number(opts.awsPeriod) : 60;
|
|
168
|
+
let memoryBytesFallback;
|
|
169
|
+
try {
|
|
170
|
+
const repo = new power_profile_repository_1.PowerProfileRepository(config_1.config.dataDir);
|
|
171
|
+
const profile = await repo.getMachineProfile('aws', opts.machine);
|
|
172
|
+
if (profile?.memoryGb) {
|
|
173
|
+
memoryBytesFallback = profile.memoryGb * 1024 * 1024 * 1024;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
try {
|
|
178
|
+
const cw = await (0, cloudwatch_1.fetchCloudWatchTimeseries)({
|
|
179
|
+
region: awsRegionValue,
|
|
180
|
+
instanceId: awsInstanceId,
|
|
181
|
+
startTime,
|
|
182
|
+
endTime,
|
|
183
|
+
periodSeconds,
|
|
184
|
+
memoryBytesFallback
|
|
185
|
+
});
|
|
186
|
+
cpuTimeseries = cw.cpuUtilization;
|
|
187
|
+
ramUsedTimeseries = cw.ramUsed;
|
|
188
|
+
ramSizeTimeseries = cw.ramTotal;
|
|
189
|
+
if (!ramSizeTimeseries.length && memoryBytesFallback) {
|
|
190
|
+
const ts = ramUsedTimeseries[0]?.timestamp || new Date().toISOString();
|
|
191
|
+
ramSizeTimeseries = [{ timestamp: ts, value: memoryBytesFallback }];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (errorValue) {
|
|
195
|
+
console.error((0, kleur_1.red)('Error fetching CloudWatch metrics:'), formatErrorMessage(errorValue));
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
if (!timeseriesProvided) {
|
|
202
|
+
console.error((0, kleur_1.red)('Error: --cpu-timeseries, --ram-used-timeseries, and --ram-size-timeseries are required'));
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
cpuTimeseries = parseTimeseriesFile(opts.cpuTimeseries);
|
|
207
|
+
ramUsedTimeseries = parseTimeseriesFile(opts.ramUsedTimeseries);
|
|
208
|
+
ramSizeTimeseries = parseTimeseriesFile(opts.ramSizeTimeseries);
|
|
209
|
+
}
|
|
45
210
|
console.log((0, kleur_1.gray)(`Loaded ${cpuTimeseries.length} CPU points, ${ramUsedTimeseries.length} RAM points`));
|
|
46
211
|
// Sort by timestamp for charts
|
|
47
212
|
const cpuSorted = [...cpuTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
48
213
|
const ramSorted = [...ramUsedTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
49
214
|
const ramSizeSorted = [...ramSizeTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
50
215
|
// CPU chart (only if more than 1 point)
|
|
51
|
-
const cpuValues = cpuSorted.map(p =>
|
|
216
|
+
const cpuValues = cpuSorted.map(p => {
|
|
217
|
+
if (provider === 'aws') {
|
|
218
|
+
return p.value <= 1 ? p.value * 100 : p.value;
|
|
219
|
+
}
|
|
220
|
+
return p.value * 100;
|
|
221
|
+
});
|
|
52
222
|
if (cpuValues.length > 1) {
|
|
53
223
|
console.log((0, kleur_1.bold)('\nCPU Utilization (%)'));
|
|
54
224
|
console.log(asciichart_1.default.plot(cpuValues, { height: 8, format: (x) => x.toFixed(1).padStart(6) }));
|
|
@@ -62,9 +232,9 @@ const runCalculate = async (opts) => {
|
|
|
62
232
|
}
|
|
63
233
|
try {
|
|
64
234
|
const { result, budget, markdown } = await (0, index_1.calculate)({
|
|
65
|
-
provider
|
|
235
|
+
provider,
|
|
66
236
|
machineType: opts.machine,
|
|
67
|
-
region:
|
|
237
|
+
region: calculationRegion,
|
|
68
238
|
cpuTimeseries,
|
|
69
239
|
ramUsedTimeseries,
|
|
70
240
|
ramSizeTimeseries,
|
|
@@ -102,20 +272,26 @@ const runCalculate = async (opts) => {
|
|
|
102
272
|
process.exitCode = 1;
|
|
103
273
|
}
|
|
104
274
|
}
|
|
105
|
-
catch (
|
|
106
|
-
console.error((0, kleur_1.red)('Error:'),
|
|
275
|
+
catch (errorValue) {
|
|
276
|
+
console.error((0, kleur_1.red)('Error:'), formatErrorMessage(errorValue));
|
|
107
277
|
process.exitCode = 1;
|
|
108
278
|
}
|
|
109
279
|
};
|
|
110
280
|
program
|
|
111
281
|
.name('gitgreen')
|
|
112
282
|
.description('GitGreen carbon calculator using real timeseries metrics')
|
|
113
|
-
.option('-p, --provider <provider>', 'Cloud provider (gcp)')
|
|
283
|
+
.option('-p, --provider <provider>', 'Cloud provider (gcp|aws)', config_1.config.defaultProvider)
|
|
114
284
|
.option('-m, --machine <type>', 'Machine type (e.g., e2-standard-4)')
|
|
115
|
-
.option('-r, --region <region>', 'Region/zone (e.g., us-central1-a)')
|
|
116
|
-
.option('--cpu-timeseries <path>', 'Path to CPU timeseries JSON (GCP Monitoring
|
|
285
|
+
.option('-r, --region <region>', 'Region/zone (e.g., us-central1-a or us-east-1)')
|
|
286
|
+
.option('--cpu-timeseries <path>', 'Path to CPU timeseries JSON (GCP Monitoring or CloudWatch)')
|
|
117
287
|
.option('--ram-used-timeseries <path>', 'Path to RAM used timeseries JSON')
|
|
118
288
|
.option('--ram-size-timeseries <path>', 'Path to RAM size timeseries JSON')
|
|
289
|
+
.option('--aws-instance-id <id>', 'AWS EC2 instance ID (for CloudWatch fetch)')
|
|
290
|
+
.option('--aws-region <region>', 'AWS region (overrides --region)')
|
|
291
|
+
.option('--aws-start-time <iso>', 'Custom start time (ISO) for CloudWatch metrics')
|
|
292
|
+
.option('--aws-end-time <iso>', 'Custom end time (ISO) for CloudWatch metrics')
|
|
293
|
+
.option('--aws-period <seconds>', 'CloudWatch metric period in seconds', v => Number(v))
|
|
294
|
+
.option('--from-cloudwatch', 'Fetch CPU/RAM metrics from CloudWatch (AWS only)')
|
|
119
295
|
.option('--budget <grams>', 'Carbon budget in grams CO2e', v => Number(v))
|
|
120
296
|
.option('--fail-on-budget', 'Exit non-zero when budget exceeded')
|
|
121
297
|
.option('--out-md <path>', 'Write markdown report')
|