@rawdash/connector-gcp-monitoring 0.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/README.md +113 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +552 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<!-- This file is generated from connector metadata by scripts/generate-connector-docs.ts. Do not edit by hand. -->
|
|
2
|
+
|
|
3
|
+
# @rawdash/connector-gcp-monitoring
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@rawdash/connector-gcp-monitoring)
|
|
6
|
+
[](https://github.com/rawdash/rawdash/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
Pull declared Cloud Monitoring metric time series (any metric type, aligner, and period) into a single metric series per query.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @rawdash/connector-gcp-monitoring
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Authentication
|
|
17
|
+
|
|
18
|
+
Authenticate against the Cloud Monitoring v3 API with a Google service account JSON key. The service account needs the Monitoring Viewer role (roles/monitoring.viewer) on the project whose metrics it reads.
|
|
19
|
+
|
|
20
|
+
1. Identify the GCP project whose metrics you want to sync.
|
|
21
|
+
2. Create a service account at Google Cloud -> IAM & Admin -> Service Accounts in that project (or grant an existing one access).
|
|
22
|
+
3. Grant the service account the Monitoring Viewer role (roles/monitoring.viewer) on the project. The API enables this role automatically for owners and editors.
|
|
23
|
+
4. Generate a JSON key for the service account and store its contents as a secret (e.g. GCP_MONITORING_SA_JSON).
|
|
24
|
+
5. Reference the key from config as serviceAccountJson: secret("GCP_MONITORING_SA_JSON") and set projectId to the same project.
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
| Field | Type | Required | Description |
|
|
29
|
+
| -------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| `projectId` | string | Yes | Google Cloud project ID whose metrics should be synced (the project that owns the monitored resources). |
|
|
31
|
+
| `serviceAccountJson` | secret | Yes | Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret. |
|
|
32
|
+
| `metricQueries` | array | Yes | Cloud Monitoring is too broad to mirror wholesale; declare the specific metrics to pull. Each query needs an id, metric type, alignment period (e.g. 300s), and a perSeriesAligner statistic, with an optional filter on resource labels. |
|
|
33
|
+
| `lookbackMinutes` | number | No | How far back to pull data points on a full sync when the host does not supply a since bound. Defaults to 180. |
|
|
34
|
+
|
|
35
|
+
## Resources
|
|
36
|
+
|
|
37
|
+
- **`<metricType>`** _(metric)_ - One metric series per declared metric query. The series name is the configured metric type (e.g. `compute.googleapis.com/instance/cpu/utilization`), so the actual keys depend on the configured `metricQueries`. Each sample carries the aligner, alignment period, query id, and metric/resource labels as attributes.
|
|
38
|
+
- Endpoint: `GET /v3/projects/{projectId}/timeSeries`
|
|
39
|
+
- Granularity: Per alignmentPeriod (a duration in seconds, e.g. 300s)
|
|
40
|
+
- Dimensions: `perSeriesAligner`, `alignmentPeriod`, `queryId`, `resourceType`
|
|
41
|
+
- Each sync replaces the full set of samples for the metric names it owns (idempotent). Distribution-valued points are dropped unless reduced to a scalar by the perSeriesAligner.
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import {
|
|
47
|
+
defineConfig,
|
|
48
|
+
defineDashboard,
|
|
49
|
+
defineMetric,
|
|
50
|
+
secret,
|
|
51
|
+
} from '@rawdash/core';
|
|
52
|
+
|
|
53
|
+
const gcpMonitoring = {
|
|
54
|
+
name: 'gcpMonitoring',
|
|
55
|
+
connectorId: 'gcp-monitoring',
|
|
56
|
+
config: {
|
|
57
|
+
projectId: 'my-project-123',
|
|
58
|
+
serviceAccountJson: secret('GCP_MONITORING_SA_JSON'),
|
|
59
|
+
metricQueries: [
|
|
60
|
+
{
|
|
61
|
+
id: 'gce_cpu',
|
|
62
|
+
metricType: 'compute.googleapis.com/instance/cpu/utilization',
|
|
63
|
+
alignmentPeriod: '300s',
|
|
64
|
+
perSeriesAligner: 'ALIGN_MEAN',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
lookbackMinutes: 180,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default defineConfig({
|
|
72
|
+
connectors: [gcpMonitoring],
|
|
73
|
+
dashboards: {
|
|
74
|
+
infra: defineDashboard({
|
|
75
|
+
widgets: {
|
|
76
|
+
cpu: {
|
|
77
|
+
kind: 'timeseries',
|
|
78
|
+
title: 'GCE CPU utilization',
|
|
79
|
+
window: '24h',
|
|
80
|
+
metric: defineMetric({
|
|
81
|
+
connector: gcpMonitoring,
|
|
82
|
+
shape: 'metric',
|
|
83
|
+
name: 'compute.googleapis.com/instance/cpu/utilization',
|
|
84
|
+
fn: 'avg',
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Rate limits
|
|
94
|
+
|
|
95
|
+
Cloud Monitoring projects.timeSeries.list is rate-limited per project; 429 / RESOURCE_EXHAUSTED responses are retried with backoff. Pagination uses nextPageToken.
|
|
96
|
+
|
|
97
|
+
## Limitations
|
|
98
|
+
|
|
99
|
+
- Cloud Monitoring is too broad to mirror wholesale; only the metrics declared in metricQueries are synced; there is no automatic metric discovery.
|
|
100
|
+
- The series name is derived from the metric type, so two queries against the same metricType with different aligners or filters share one series name and are distinguished only by sample attributes.
|
|
101
|
+
- Each query alignmentPeriod must be expressed as a duration in seconds, e.g. 60s or 300s.
|
|
102
|
+
- A full sync uses lookbackMinutes; a latest sync uses a short window covering the last few alignment periods.
|
|
103
|
+
- Distribution-valued metrics (e.g. latency histograms) require a perSeriesAligner that reduces them to a scalar (ALIGN_PERCENTILE_99, ALIGN_MEAN, etc.); raw distributions are not stored.
|
|
104
|
+
|
|
105
|
+
## Links
|
|
106
|
+
|
|
107
|
+
- [Rawdash docs](https://rawdash.dev/docs/connectors/)
|
|
108
|
+
- [Google Cloud API docs](https://cloud.google.com/monitoring/api/v3)
|
|
109
|
+
- [GitHub](https://github.com/rawdash/rawdash)
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult, ConnectorDoc, MetricSample } from '@rawdash/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
declare const configFields: z.ZodObject<{
|
|
5
|
+
metricQueries: z.ZodArray<z.ZodObject<{
|
|
6
|
+
id: z.ZodString;
|
|
7
|
+
metricType: z.ZodString;
|
|
8
|
+
filter: z.ZodOptional<z.ZodString>;
|
|
9
|
+
alignmentPeriod: z.ZodString;
|
|
10
|
+
perSeriesAligner: z.ZodEnum<{
|
|
11
|
+
ALIGN_NONE: "ALIGN_NONE";
|
|
12
|
+
ALIGN_DELTA: "ALIGN_DELTA";
|
|
13
|
+
ALIGN_RATE: "ALIGN_RATE";
|
|
14
|
+
ALIGN_INTERPOLATE: "ALIGN_INTERPOLATE";
|
|
15
|
+
ALIGN_NEXT_OLDER: "ALIGN_NEXT_OLDER";
|
|
16
|
+
ALIGN_MIN: "ALIGN_MIN";
|
|
17
|
+
ALIGN_MAX: "ALIGN_MAX";
|
|
18
|
+
ALIGN_MEAN: "ALIGN_MEAN";
|
|
19
|
+
ALIGN_COUNT: "ALIGN_COUNT";
|
|
20
|
+
ALIGN_SUM: "ALIGN_SUM";
|
|
21
|
+
ALIGN_STDDEV: "ALIGN_STDDEV";
|
|
22
|
+
ALIGN_COUNT_TRUE: "ALIGN_COUNT_TRUE";
|
|
23
|
+
ALIGN_COUNT_FALSE: "ALIGN_COUNT_FALSE";
|
|
24
|
+
ALIGN_FRACTION_TRUE: "ALIGN_FRACTION_TRUE";
|
|
25
|
+
ALIGN_PERCENTILE_99: "ALIGN_PERCENTILE_99";
|
|
26
|
+
ALIGN_PERCENTILE_95: "ALIGN_PERCENTILE_95";
|
|
27
|
+
ALIGN_PERCENTILE_50: "ALIGN_PERCENTILE_50";
|
|
28
|
+
ALIGN_PERCENTILE_05: "ALIGN_PERCENTILE_05";
|
|
29
|
+
ALIGN_PERCENT_CHANGE: "ALIGN_PERCENT_CHANGE";
|
|
30
|
+
}>;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
lookbackMinutes: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
serviceAccountJson: z.ZodObject<{
|
|
34
|
+
$secret: z.ZodString;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
projectId: z.ZodString;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
declare const doc: ConnectorDoc;
|
|
39
|
+
interface GcpMonitoringMetricQuery {
|
|
40
|
+
id: string;
|
|
41
|
+
metricType: string;
|
|
42
|
+
filter?: string;
|
|
43
|
+
alignmentPeriod: string;
|
|
44
|
+
perSeriesAligner: string;
|
|
45
|
+
}
|
|
46
|
+
interface GcpMonitoringSettings {
|
|
47
|
+
projectId: string;
|
|
48
|
+
metricQueries: GcpMonitoringMetricQuery[];
|
|
49
|
+
lookbackMinutes?: number;
|
|
50
|
+
}
|
|
51
|
+
declare const gcpMonitoringCredentials: {
|
|
52
|
+
serviceAccountJson: {
|
|
53
|
+
description: string;
|
|
54
|
+
auth: "required";
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
type GcpMonitoringCredentials = typeof gcpMonitoringCredentials;
|
|
58
|
+
declare const pointSchema: z.ZodObject<{
|
|
59
|
+
interval: z.ZodObject<{
|
|
60
|
+
startTime: z.ZodOptional<z.ZodISODateTime>;
|
|
61
|
+
endTime: z.ZodISODateTime;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
value: z.ZodObject<{
|
|
64
|
+
doubleValue: z.ZodOptional<z.ZodNumber>;
|
|
65
|
+
int64Value: z.ZodOptional<z.ZodString>;
|
|
66
|
+
boolValue: z.ZodOptional<z.ZodBoolean>;
|
|
67
|
+
stringValue: z.ZodOptional<z.ZodString>;
|
|
68
|
+
distributionValue: z.ZodOptional<z.ZodUnknown>;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
declare const timeSeriesSchema: z.ZodObject<{
|
|
72
|
+
metric: z.ZodObject<{
|
|
73
|
+
type: z.ZodString;
|
|
74
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
75
|
+
}, z.core.$strip>;
|
|
76
|
+
resource: z.ZodOptional<z.ZodObject<{
|
|
77
|
+
type: z.ZodOptional<z.ZodString>;
|
|
78
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
79
|
+
}, z.core.$strip>>;
|
|
80
|
+
valueType: z.ZodOptional<z.ZodString>;
|
|
81
|
+
metricKind: z.ZodOptional<z.ZodString>;
|
|
82
|
+
points: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
83
|
+
interval: z.ZodObject<{
|
|
84
|
+
startTime: z.ZodOptional<z.ZodISODateTime>;
|
|
85
|
+
endTime: z.ZodISODateTime;
|
|
86
|
+
}, z.core.$strip>;
|
|
87
|
+
value: z.ZodObject<{
|
|
88
|
+
doubleValue: z.ZodOptional<z.ZodNumber>;
|
|
89
|
+
int64Value: z.ZodOptional<z.ZodString>;
|
|
90
|
+
boolValue: z.ZodOptional<z.ZodBoolean>;
|
|
91
|
+
stringValue: z.ZodOptional<z.ZodString>;
|
|
92
|
+
distributionValue: z.ZodOptional<z.ZodUnknown>;
|
|
93
|
+
}, z.core.$strip>;
|
|
94
|
+
}, z.core.$strip>>>;
|
|
95
|
+
}, z.core.$strip>;
|
|
96
|
+
declare const gcpMonitoringResources: {
|
|
97
|
+
readonly '<metricType>': {
|
|
98
|
+
readonly shape: "metric";
|
|
99
|
+
readonly dynamic: true;
|
|
100
|
+
readonly description: "One metric series per declared metric query. The series name is the configured metric type (e.g. `compute.googleapis.com/instance/cpu/utilization`), so the actual keys depend on the configured `metricQueries`. Each sample carries the aligner, alignment period, query id, and metric/resource labels as attributes.";
|
|
101
|
+
readonly endpoint: "GET /v3/projects/{projectId}/timeSeries";
|
|
102
|
+
readonly granularity: "Per alignmentPeriod (a duration in seconds, e.g. 300s)";
|
|
103
|
+
readonly notes: "Each sync replaces the full set of samples for the metric names it owns (idempotent). Distribution-valued points are dropped unless reduced to a scalar by the perSeriesAligner.";
|
|
104
|
+
readonly dimensions: [{
|
|
105
|
+
readonly name: "perSeriesAligner";
|
|
106
|
+
readonly description: "The Cloud Monitoring statistic requested for the query, e.g. ALIGN_MEAN, ALIGN_SUM, or ALIGN_PERCENTILE_99.";
|
|
107
|
+
}, {
|
|
108
|
+
readonly name: "alignmentPeriod";
|
|
109
|
+
readonly description: "The aggregation alignment period as configured, e.g. 300s.";
|
|
110
|
+
}, {
|
|
111
|
+
readonly name: "queryId";
|
|
112
|
+
readonly description: "The configured id of the metric query that produced the sample.";
|
|
113
|
+
}, {
|
|
114
|
+
readonly name: "resourceType";
|
|
115
|
+
readonly description: "The monitored resource type the sample originated from (e.g. gce_instance).";
|
|
116
|
+
}];
|
|
117
|
+
readonly responses: {
|
|
118
|
+
readonly oauth_token: z.ZodObject<{
|
|
119
|
+
access_token: z.ZodString;
|
|
120
|
+
expires_in: z.ZodOptional<z.ZodNumber>;
|
|
121
|
+
}, z.core.$strip>;
|
|
122
|
+
readonly time_series: z.ZodObject<{
|
|
123
|
+
timeSeries: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
124
|
+
metric: z.ZodObject<{
|
|
125
|
+
type: z.ZodString;
|
|
126
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
127
|
+
}, z.core.$strip>;
|
|
128
|
+
resource: z.ZodOptional<z.ZodObject<{
|
|
129
|
+
type: z.ZodOptional<z.ZodString>;
|
|
130
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
131
|
+
}, z.core.$strip>>;
|
|
132
|
+
valueType: z.ZodOptional<z.ZodString>;
|
|
133
|
+
metricKind: z.ZodOptional<z.ZodString>;
|
|
134
|
+
points: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
135
|
+
interval: z.ZodObject<{
|
|
136
|
+
startTime: z.ZodOptional<z.ZodISODateTime>;
|
|
137
|
+
endTime: z.ZodISODateTime;
|
|
138
|
+
}, z.core.$strip>;
|
|
139
|
+
value: z.ZodObject<{
|
|
140
|
+
doubleValue: z.ZodOptional<z.ZodNumber>;
|
|
141
|
+
int64Value: z.ZodOptional<z.ZodString>;
|
|
142
|
+
boolValue: z.ZodOptional<z.ZodBoolean>;
|
|
143
|
+
stringValue: z.ZodOptional<z.ZodString>;
|
|
144
|
+
distributionValue: z.ZodOptional<z.ZodUnknown>;
|
|
145
|
+
}, z.core.$strip>;
|
|
146
|
+
}, z.core.$strip>>>;
|
|
147
|
+
}, z.core.$strip>>>;
|
|
148
|
+
nextPageToken: z.ZodOptional<z.ZodString>;
|
|
149
|
+
}, z.core.$strip>;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
declare const id = "gcp-monitoring";
|
|
154
|
+
declare class GcpMonitoringConnector extends BaseConnector<GcpMonitoringSettings, GcpMonitoringCredentials> {
|
|
155
|
+
static readonly id = "gcp-monitoring";
|
|
156
|
+
static readonly resources: {
|
|
157
|
+
readonly '<metricType>': {
|
|
158
|
+
readonly shape: "metric";
|
|
159
|
+
readonly dynamic: true;
|
|
160
|
+
readonly description: "One metric series per declared metric query. The series name is the configured metric type (e.g. `compute.googleapis.com/instance/cpu/utilization`), so the actual keys depend on the configured `metricQueries`. Each sample carries the aligner, alignment period, query id, and metric/resource labels as attributes.";
|
|
161
|
+
readonly endpoint: "GET /v3/projects/{projectId}/timeSeries";
|
|
162
|
+
readonly granularity: "Per alignmentPeriod (a duration in seconds, e.g. 300s)";
|
|
163
|
+
readonly notes: "Each sync replaces the full set of samples for the metric names it owns (idempotent). Distribution-valued points are dropped unless reduced to a scalar by the perSeriesAligner.";
|
|
164
|
+
readonly dimensions: [{
|
|
165
|
+
readonly name: "perSeriesAligner";
|
|
166
|
+
readonly description: "The Cloud Monitoring statistic requested for the query, e.g. ALIGN_MEAN, ALIGN_SUM, or ALIGN_PERCENTILE_99.";
|
|
167
|
+
}, {
|
|
168
|
+
readonly name: "alignmentPeriod";
|
|
169
|
+
readonly description: "The aggregation alignment period as configured, e.g. 300s.";
|
|
170
|
+
}, {
|
|
171
|
+
readonly name: "queryId";
|
|
172
|
+
readonly description: "The configured id of the metric query that produced the sample.";
|
|
173
|
+
}, {
|
|
174
|
+
readonly name: "resourceType";
|
|
175
|
+
readonly description: "The monitored resource type the sample originated from (e.g. gce_instance).";
|
|
176
|
+
}];
|
|
177
|
+
readonly responses: {
|
|
178
|
+
readonly oauth_token: z.ZodObject<{
|
|
179
|
+
access_token: z.ZodString;
|
|
180
|
+
expires_in: z.ZodOptional<z.ZodNumber>;
|
|
181
|
+
}, z.core.$strip>;
|
|
182
|
+
readonly time_series: z.ZodObject<{
|
|
183
|
+
timeSeries: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
184
|
+
metric: z.ZodObject<{
|
|
185
|
+
type: z.ZodString;
|
|
186
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
187
|
+
}, z.core.$strip>;
|
|
188
|
+
resource: z.ZodOptional<z.ZodObject<{
|
|
189
|
+
type: z.ZodOptional<z.ZodString>;
|
|
190
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
191
|
+
}, z.core.$strip>>;
|
|
192
|
+
valueType: z.ZodOptional<z.ZodString>;
|
|
193
|
+
metricKind: z.ZodOptional<z.ZodString>;
|
|
194
|
+
points: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
195
|
+
interval: z.ZodObject<{
|
|
196
|
+
startTime: z.ZodOptional<z.ZodISODateTime>;
|
|
197
|
+
endTime: z.ZodISODateTime;
|
|
198
|
+
}, z.core.$strip>;
|
|
199
|
+
value: z.ZodObject<{
|
|
200
|
+
doubleValue: z.ZodOptional<z.ZodNumber>;
|
|
201
|
+
int64Value: z.ZodOptional<z.ZodString>;
|
|
202
|
+
boolValue: z.ZodOptional<z.ZodBoolean>;
|
|
203
|
+
stringValue: z.ZodOptional<z.ZodString>;
|
|
204
|
+
distributionValue: z.ZodOptional<z.ZodUnknown>;
|
|
205
|
+
}, z.core.$strip>;
|
|
206
|
+
}, z.core.$strip>>>;
|
|
207
|
+
}, z.core.$strip>>>;
|
|
208
|
+
nextPageToken: z.ZodOptional<z.ZodString>;
|
|
209
|
+
}, z.core.$strip>;
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
static readonly schemas: {
|
|
214
|
+
readonly oauth_token: z.ZodObject<{
|
|
215
|
+
access_token: z.ZodString;
|
|
216
|
+
expires_in: z.ZodOptional<z.ZodNumber>;
|
|
217
|
+
}, z.core.$strip>;
|
|
218
|
+
readonly time_series: z.ZodObject<{
|
|
219
|
+
timeSeries: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
220
|
+
metric: z.ZodObject<{
|
|
221
|
+
type: z.ZodString;
|
|
222
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
223
|
+
}, z.core.$strip>;
|
|
224
|
+
resource: z.ZodOptional<z.ZodObject<{
|
|
225
|
+
type: z.ZodOptional<z.ZodString>;
|
|
226
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
227
|
+
}, z.core.$strip>>;
|
|
228
|
+
valueType: z.ZodOptional<z.ZodString>;
|
|
229
|
+
metricKind: z.ZodOptional<z.ZodString>;
|
|
230
|
+
points: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
231
|
+
interval: z.ZodObject<{
|
|
232
|
+
startTime: z.ZodOptional<z.ZodISODateTime>;
|
|
233
|
+
endTime: z.ZodISODateTime;
|
|
234
|
+
}, z.core.$strip>;
|
|
235
|
+
value: z.ZodObject<{
|
|
236
|
+
doubleValue: z.ZodOptional<z.ZodNumber>;
|
|
237
|
+
int64Value: z.ZodOptional<z.ZodString>;
|
|
238
|
+
boolValue: z.ZodOptional<z.ZodBoolean>;
|
|
239
|
+
stringValue: z.ZodOptional<z.ZodString>;
|
|
240
|
+
distributionValue: z.ZodOptional<z.ZodUnknown>;
|
|
241
|
+
}, z.core.$strip>;
|
|
242
|
+
}, z.core.$strip>>>;
|
|
243
|
+
}, z.core.$strip>>>;
|
|
244
|
+
nextPageToken: z.ZodOptional<z.ZodString>;
|
|
245
|
+
}, z.core.$strip>;
|
|
246
|
+
} & Readonly<Record<string, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
|
|
247
|
+
static create(input: unknown, ctx?: ConnectorContext): GcpMonitoringConnector;
|
|
248
|
+
readonly id = "gcp-monitoring";
|
|
249
|
+
readonly credentials: {
|
|
250
|
+
serviceAccountJson: {
|
|
251
|
+
description: string;
|
|
252
|
+
auth: "required";
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
private cachedToken;
|
|
256
|
+
private getAccessToken;
|
|
257
|
+
private computeWindow;
|
|
258
|
+
private buildFilter;
|
|
259
|
+
private listTimeSeries;
|
|
260
|
+
sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
|
|
261
|
+
}
|
|
262
|
+
declare function parseDurationSeconds(duration: string): number | null;
|
|
263
|
+
declare function pointToSample(query: GcpMonitoringMetricQuery, series: z.infer<typeof timeSeriesSchema>, point: z.infer<typeof pointSchema>): MetricSample | null;
|
|
264
|
+
|
|
265
|
+
export { GcpMonitoringConnector, type GcpMonitoringMetricQuery, type GcpMonitoringSettings, configFields, GcpMonitoringConnector as default, doc, id, parseDurationSeconds, pointToSample, gcpMonitoringResources as resources };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// ../gcp-shared/dist/index.js
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { z as z2 } from "zod";
|
|
4
|
+
var serviceAccountKeySchema = z.object({
|
|
5
|
+
client_email: z.string().min(1),
|
|
6
|
+
private_key: z.string().min(1),
|
|
7
|
+
token_uri: z.string().url().optional(),
|
|
8
|
+
project_id: z.string().optional()
|
|
9
|
+
});
|
|
10
|
+
var tokenResponseSchema = z.object({
|
|
11
|
+
access_token: z.string().min(1),
|
|
12
|
+
expires_in: z.number().int().positive().optional()
|
|
13
|
+
});
|
|
14
|
+
function base64urlFromBytes(bytes) {
|
|
15
|
+
let binary = "";
|
|
16
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
17
|
+
binary += String.fromCharCode(bytes[i]);
|
|
18
|
+
}
|
|
19
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
20
|
+
}
|
|
21
|
+
function base64urlFromString(str) {
|
|
22
|
+
return base64urlFromBytes(new TextEncoder().encode(str));
|
|
23
|
+
}
|
|
24
|
+
async function signRS256JWT(payload, privateKeyPem) {
|
|
25
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
26
|
+
const headerB64 = base64urlFromString(JSON.stringify(header));
|
|
27
|
+
const payloadB64 = base64urlFromString(JSON.stringify(payload));
|
|
28
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
29
|
+
const pemContent = privateKeyPem.replace(/-----BEGIN PRIVATE KEY-----/g, "").replace(/-----END PRIVATE KEY-----/g, "").replace(/\s/g, "");
|
|
30
|
+
const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));
|
|
31
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
32
|
+
"pkcs8",
|
|
33
|
+
der,
|
|
34
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
35
|
+
false,
|
|
36
|
+
["sign"]
|
|
37
|
+
);
|
|
38
|
+
const signature = await globalThis.crypto.subtle.sign(
|
|
39
|
+
"RSASSA-PKCS1-v1_5",
|
|
40
|
+
key,
|
|
41
|
+
new TextEncoder().encode(signingInput)
|
|
42
|
+
);
|
|
43
|
+
return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;
|
|
44
|
+
}
|
|
45
|
+
function parseServiceAccountJson(value) {
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
if (trimmed.startsWith("{")) {
|
|
48
|
+
return serviceAccountKeySchema.parse(JSON.parse(trimmed));
|
|
49
|
+
}
|
|
50
|
+
const binary = atob(trimmed);
|
|
51
|
+
const bytes = new Uint8Array(binary.length);
|
|
52
|
+
for (let i = 0; i < binary.length; i++) {
|
|
53
|
+
bytes[i] = binary.charCodeAt(i);
|
|
54
|
+
}
|
|
55
|
+
const decoded = new TextDecoder().decode(bytes);
|
|
56
|
+
return serviceAccountKeySchema.parse(JSON.parse(decoded));
|
|
57
|
+
}
|
|
58
|
+
async function buildServiceAccountJwt(serviceAccountJson, scope) {
|
|
59
|
+
const sa = parseServiceAccountJson(serviceAccountJson);
|
|
60
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
61
|
+
const jwt = await signRS256JWT(
|
|
62
|
+
{
|
|
63
|
+
iss: sa.client_email,
|
|
64
|
+
scope,
|
|
65
|
+
aud: sa.token_uri ?? "https://oauth2.googleapis.com/token",
|
|
66
|
+
exp: now + 3600,
|
|
67
|
+
iat: now
|
|
68
|
+
},
|
|
69
|
+
sa.private_key
|
|
70
|
+
);
|
|
71
|
+
const body = new URLSearchParams({
|
|
72
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
73
|
+
assertion: jwt
|
|
74
|
+
}).toString();
|
|
75
|
+
return {
|
|
76
|
+
url: sa.token_uri ?? "https://oauth2.googleapis.com/token",
|
|
77
|
+
body
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var gcpAuthConfigShape = {
|
|
81
|
+
serviceAccountJson: z2.object({ $secret: z2.string().trim().min(1) }).meta({
|
|
82
|
+
label: "Service Account JSON",
|
|
83
|
+
description: "Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.",
|
|
84
|
+
secret: true
|
|
85
|
+
})
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ../../connector-shared/dist/index.js
|
|
89
|
+
var HttpClientError = class extends Error {
|
|
90
|
+
response;
|
|
91
|
+
constructor(message, response) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.name = new.target.name;
|
|
94
|
+
this.response = response;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var AuthError = class extends HttpClientError {
|
|
98
|
+
kind = "auth";
|
|
99
|
+
};
|
|
100
|
+
var HTTP_CLIENT_VERSION = "0.0.0";
|
|
101
|
+
var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
102
|
+
function connectorUserAgent(connectorId) {
|
|
103
|
+
return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
104
|
+
}
|
|
105
|
+
function parseEpoch(value, unit) {
|
|
106
|
+
if (value === null || value === void 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (unit === "iso") {
|
|
110
|
+
if (typeof value !== "string") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const ms = new Date(value).getTime();
|
|
114
|
+
return Number.isFinite(ms) ? ms : null;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
120
|
+
if (!Number.isFinite(n)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const result = unit === "s" ? n * 1e3 : n;
|
|
124
|
+
return Number.isFinite(result) ? result : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/gcp-monitoring.ts
|
|
128
|
+
import {
|
|
129
|
+
BaseConnector,
|
|
130
|
+
defineConfigFields,
|
|
131
|
+
defineConnectorDoc,
|
|
132
|
+
defineResources,
|
|
133
|
+
schemasFromResources
|
|
134
|
+
} from "@rawdash/core";
|
|
135
|
+
import { z as z3 } from "zod";
|
|
136
|
+
var metricQuerySchema = z3.object({
|
|
137
|
+
id: z3.string().regex(
|
|
138
|
+
/^[a-z][a-zA-Z0-9_]*$/,
|
|
139
|
+
"Monitoring query id must start with a lowercase letter and contain only letters, digits, and underscores"
|
|
140
|
+
),
|
|
141
|
+
metricType: z3.string().min(1).meta({
|
|
142
|
+
description: "Fully-qualified Cloud Monitoring metric type, e.g. compute.googleapis.com/instance/cpu/utilization."
|
|
143
|
+
}),
|
|
144
|
+
filter: z3.string().optional().meta({
|
|
145
|
+
description: 'Optional additional filter combined with metric.type using AND, e.g. resource.labels.zone="us-central1-a".'
|
|
146
|
+
}),
|
|
147
|
+
alignmentPeriod: z3.string().regex(
|
|
148
|
+
/^\d+s$/,
|
|
149
|
+
"alignmentPeriod must be a duration in seconds, e.g. 60s or 300s"
|
|
150
|
+
).meta({
|
|
151
|
+
description: "Aggregation alignment period as a duration in seconds, e.g. 300s."
|
|
152
|
+
}),
|
|
153
|
+
perSeriesAligner: z3.enum([
|
|
154
|
+
"ALIGN_NONE",
|
|
155
|
+
"ALIGN_DELTA",
|
|
156
|
+
"ALIGN_RATE",
|
|
157
|
+
"ALIGN_INTERPOLATE",
|
|
158
|
+
"ALIGN_NEXT_OLDER",
|
|
159
|
+
"ALIGN_MIN",
|
|
160
|
+
"ALIGN_MAX",
|
|
161
|
+
"ALIGN_MEAN",
|
|
162
|
+
"ALIGN_COUNT",
|
|
163
|
+
"ALIGN_SUM",
|
|
164
|
+
"ALIGN_STDDEV",
|
|
165
|
+
"ALIGN_COUNT_TRUE",
|
|
166
|
+
"ALIGN_COUNT_FALSE",
|
|
167
|
+
"ALIGN_FRACTION_TRUE",
|
|
168
|
+
"ALIGN_PERCENTILE_99",
|
|
169
|
+
"ALIGN_PERCENTILE_95",
|
|
170
|
+
"ALIGN_PERCENTILE_50",
|
|
171
|
+
"ALIGN_PERCENTILE_05",
|
|
172
|
+
"ALIGN_PERCENT_CHANGE"
|
|
173
|
+
]).meta({
|
|
174
|
+
description: "Cloud Monitoring perSeriesAligner statistic, e.g. ALIGN_MEAN, ALIGN_SUM, ALIGN_PERCENTILE_99."
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
var configFields = defineConfigFields(
|
|
178
|
+
z3.object({
|
|
179
|
+
projectId: z3.string().min(1).meta({
|
|
180
|
+
label: "GCP project ID",
|
|
181
|
+
description: "Google Cloud project ID whose metrics should be synced (the project that owns the monitored resources).",
|
|
182
|
+
placeholder: "my-project-123"
|
|
183
|
+
}),
|
|
184
|
+
...gcpAuthConfigShape,
|
|
185
|
+
metricQueries: z3.array(metricQuerySchema).nonempty().meta({
|
|
186
|
+
label: "Metric queries",
|
|
187
|
+
description: "Cloud Monitoring is too broad to mirror wholesale; declare the specific metrics to pull. Each query needs an id, metric type, alignment period (e.g. 300s), and a perSeriesAligner statistic, with an optional filter on resource labels."
|
|
188
|
+
}),
|
|
189
|
+
lookbackMinutes: z3.number().int().positive().max(40320).optional().meta({
|
|
190
|
+
label: "Lookback (minutes)",
|
|
191
|
+
description: "How far back to pull data points on a full sync when the host does not supply a since bound. Defaults to 180.",
|
|
192
|
+
placeholder: "180"
|
|
193
|
+
})
|
|
194
|
+
}).refine(
|
|
195
|
+
(cfg) => new Set(cfg.metricQueries.map((q) => q.id)).size === cfg.metricQueries.length,
|
|
196
|
+
{
|
|
197
|
+
path: ["metricQueries"],
|
|
198
|
+
message: "Each metric query id must be unique"
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
var doc = defineConnectorDoc({
|
|
203
|
+
displayName: "Google Cloud Monitoring",
|
|
204
|
+
category: "infrastructure",
|
|
205
|
+
brandColor: "#4285F4",
|
|
206
|
+
tagline: "Pull declared Cloud Monitoring metric time series (any metric type, aligner, and period) into a single metric series per query.",
|
|
207
|
+
rateLimit: "Cloud Monitoring projects.timeSeries.list is rate-limited per project; 429 / RESOURCE_EXHAUSTED responses are retried with backoff. Pagination uses nextPageToken.",
|
|
208
|
+
vendor: {
|
|
209
|
+
name: "Google Cloud",
|
|
210
|
+
apiDocs: "https://cloud.google.com/monitoring/api/v3",
|
|
211
|
+
website: "https://cloud.google.com/monitoring"
|
|
212
|
+
},
|
|
213
|
+
auth: {
|
|
214
|
+
summary: "Authenticate against the Cloud Monitoring v3 API with a Google service account JSON key. The service account needs the Monitoring Viewer role (roles/monitoring.viewer) on the project whose metrics it reads.",
|
|
215
|
+
setup: [
|
|
216
|
+
"Identify the GCP project whose metrics you want to sync.",
|
|
217
|
+
"Create a service account at Google Cloud -> IAM & Admin -> Service Accounts in that project (or grant an existing one access).",
|
|
218
|
+
"Grant the service account the Monitoring Viewer role (roles/monitoring.viewer) on the project. The API enables this role automatically for owners and editors.",
|
|
219
|
+
"Generate a JSON key for the service account and store its contents as a secret (e.g. GCP_MONITORING_SA_JSON).",
|
|
220
|
+
'Reference the key from config as serviceAccountJson: secret("GCP_MONITORING_SA_JSON") and set projectId to the same project.'
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
limitations: [
|
|
224
|
+
"Cloud Monitoring is too broad to mirror wholesale; only the metrics declared in metricQueries are synced; there is no automatic metric discovery.",
|
|
225
|
+
"The series name is derived from the metric type, so two queries against the same metricType with different aligners or filters share one series name and are distinguished only by sample attributes.",
|
|
226
|
+
"Each query alignmentPeriod must be expressed as a duration in seconds, e.g. 60s or 300s.",
|
|
227
|
+
"A full sync uses lookbackMinutes; a latest sync uses a short window covering the last few alignment periods.",
|
|
228
|
+
"Distribution-valued metrics (e.g. latency histograms) require a perSeriesAligner that reduces them to a scalar (ALIGN_PERCENTILE_99, ALIGN_MEAN, etc.); raw distributions are not stored."
|
|
229
|
+
]
|
|
230
|
+
});
|
|
231
|
+
var gcpMonitoringCredentials = {
|
|
232
|
+
serviceAccountJson: {
|
|
233
|
+
description: "Google service account JSON key (raw JSON or base64)",
|
|
234
|
+
auth: "required"
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var int64String = z3.string().regex(/^-?\d+$/);
|
|
238
|
+
var isoTimestamp = z3.iso.datetime();
|
|
239
|
+
var pointValue = z3.object({
|
|
240
|
+
doubleValue: z3.number().optional(),
|
|
241
|
+
int64Value: int64String.optional(),
|
|
242
|
+
boolValue: z3.boolean().optional(),
|
|
243
|
+
stringValue: z3.string().optional(),
|
|
244
|
+
distributionValue: z3.unknown().optional()
|
|
245
|
+
}).refine(
|
|
246
|
+
(v) => v.doubleValue !== void 0 || v.int64Value !== void 0 || v.boolValue !== void 0 || v.stringValue !== void 0 || v.distributionValue !== void 0,
|
|
247
|
+
{
|
|
248
|
+
message: "point value must carry at least one supported value field"
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
var pointSchema = z3.object({
|
|
252
|
+
interval: z3.object({
|
|
253
|
+
startTime: isoTimestamp.optional(),
|
|
254
|
+
endTime: isoTimestamp
|
|
255
|
+
}),
|
|
256
|
+
value: pointValue
|
|
257
|
+
});
|
|
258
|
+
var timeSeriesSchema = z3.object({
|
|
259
|
+
metric: z3.object({
|
|
260
|
+
type: z3.string(),
|
|
261
|
+
labels: z3.record(z3.string(), z3.string()).optional()
|
|
262
|
+
}),
|
|
263
|
+
resource: z3.object({
|
|
264
|
+
type: z3.string().optional(),
|
|
265
|
+
labels: z3.record(z3.string(), z3.string()).optional()
|
|
266
|
+
}).optional(),
|
|
267
|
+
valueType: z3.string().optional(),
|
|
268
|
+
metricKind: z3.string().optional(),
|
|
269
|
+
points: z3.array(pointSchema).optional()
|
|
270
|
+
});
|
|
271
|
+
var listTimeSeriesResponseSchema = z3.object({
|
|
272
|
+
timeSeries: z3.array(timeSeriesSchema).optional(),
|
|
273
|
+
nextPageToken: z3.string().optional()
|
|
274
|
+
});
|
|
275
|
+
var gcpMonitoringResources = defineResources({
|
|
276
|
+
"<metricType>": {
|
|
277
|
+
shape: "metric",
|
|
278
|
+
dynamic: true,
|
|
279
|
+
description: "One metric series per declared metric query. The series name is the configured metric type (e.g. `compute.googleapis.com/instance/cpu/utilization`), so the actual keys depend on the configured `metricQueries`. Each sample carries the aligner, alignment period, query id, and metric/resource labels as attributes.",
|
|
280
|
+
endpoint: "GET /v3/projects/{projectId}/timeSeries",
|
|
281
|
+
granularity: "Per alignmentPeriod (a duration in seconds, e.g. 300s)",
|
|
282
|
+
notes: "Each sync replaces the full set of samples for the metric names it owns (idempotent). Distribution-valued points are dropped unless reduced to a scalar by the perSeriesAligner.",
|
|
283
|
+
dimensions: [
|
|
284
|
+
{
|
|
285
|
+
name: "perSeriesAligner",
|
|
286
|
+
description: "The Cloud Monitoring statistic requested for the query, e.g. ALIGN_MEAN, ALIGN_SUM, or ALIGN_PERCENTILE_99."
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "alignmentPeriod",
|
|
290
|
+
description: "The aggregation alignment period as configured, e.g. 300s."
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "queryId",
|
|
294
|
+
description: "The configured id of the metric query that produced the sample."
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "resourceType",
|
|
298
|
+
description: "The monitored resource type the sample originated from (e.g. gce_instance)."
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
responses: {
|
|
302
|
+
oauth_token: tokenResponseSchema,
|
|
303
|
+
time_series: listTimeSeriesResponseSchema
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
var MONITORING_API_BASE = "https://monitoring.googleapis.com/v3";
|
|
308
|
+
var MONITORING_SCOPE = "https://www.googleapis.com/auth/monitoring.read";
|
|
309
|
+
var PAGE_SIZE = 1e3;
|
|
310
|
+
var DEFAULT_LOOKBACK_MINUTES = 180;
|
|
311
|
+
var MS_PER_MINUTE = 6e4;
|
|
312
|
+
var id = "gcp-monitoring";
|
|
313
|
+
var GcpMonitoringConnector = class _GcpMonitoringConnector extends BaseConnector {
|
|
314
|
+
static id = id;
|
|
315
|
+
static resources = gcpMonitoringResources;
|
|
316
|
+
static schemas = schemasFromResources(gcpMonitoringResources);
|
|
317
|
+
static create(input, ctx) {
|
|
318
|
+
const parsed = configFields.parse(input);
|
|
319
|
+
return new _GcpMonitoringConnector(
|
|
320
|
+
{
|
|
321
|
+
projectId: parsed.projectId,
|
|
322
|
+
metricQueries: parsed.metricQueries,
|
|
323
|
+
lookbackMinutes: parsed.lookbackMinutes
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
serviceAccountJson: parsed.serviceAccountJson
|
|
327
|
+
},
|
|
328
|
+
ctx
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
id = id;
|
|
332
|
+
credentials = gcpMonitoringCredentials;
|
|
333
|
+
cachedToken = null;
|
|
334
|
+
async getAccessToken(signal) {
|
|
335
|
+
if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {
|
|
336
|
+
return this.cachedToken.token;
|
|
337
|
+
}
|
|
338
|
+
const { serviceAccountJson } = this.creds;
|
|
339
|
+
if (!serviceAccountJson) {
|
|
340
|
+
throw new AuthError(`${this.id}: missing serviceAccountJson credential`);
|
|
341
|
+
}
|
|
342
|
+
const { url, body } = await buildServiceAccountJwt(
|
|
343
|
+
serviceAccountJson,
|
|
344
|
+
MONITORING_SCOPE
|
|
345
|
+
);
|
|
346
|
+
const res = await this.post(url, {
|
|
347
|
+
resource: "oauth_token",
|
|
348
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
349
|
+
body,
|
|
350
|
+
signal
|
|
351
|
+
});
|
|
352
|
+
const expiresIn = res.body.expires_in ?? 3600;
|
|
353
|
+
this.cachedToken = {
|
|
354
|
+
token: res.body.access_token,
|
|
355
|
+
expiresAt: Date.now() + (expiresIn - 60) * 1e3
|
|
356
|
+
};
|
|
357
|
+
return this.cachedToken.token;
|
|
358
|
+
}
|
|
359
|
+
computeWindow(options) {
|
|
360
|
+
const endMs = Date.now();
|
|
361
|
+
if (options.since) {
|
|
362
|
+
const sinceMs = parseEpoch(options.since, "iso");
|
|
363
|
+
if (sinceMs !== null) {
|
|
364
|
+
return { startMs: Math.min(sinceMs, endMs), endMs };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (options.mode === "latest") {
|
|
368
|
+
const maxPeriodSec = Math.max(
|
|
369
|
+
...this.settings.metricQueries.map(
|
|
370
|
+
(q) => parseDurationSeconds(q.alignmentPeriod) ?? 60
|
|
371
|
+
),
|
|
372
|
+
60
|
|
373
|
+
);
|
|
374
|
+
return { startMs: endMs - maxPeriodSec * 3 * 1e3, endMs };
|
|
375
|
+
}
|
|
376
|
+
const lookback = this.settings.lookbackMinutes ?? DEFAULT_LOOKBACK_MINUTES;
|
|
377
|
+
return { startMs: endMs - lookback * MS_PER_MINUTE, endMs };
|
|
378
|
+
}
|
|
379
|
+
buildFilter(query) {
|
|
380
|
+
const base = `metric.type = "${query.metricType}"`;
|
|
381
|
+
if (query.filter && query.filter.trim().length > 0) {
|
|
382
|
+
return `${base} AND ${query.filter}`;
|
|
383
|
+
}
|
|
384
|
+
return base;
|
|
385
|
+
}
|
|
386
|
+
async listTimeSeries(accessToken, query, startMs, endMs, pageToken, signal) {
|
|
387
|
+
const params = new URLSearchParams();
|
|
388
|
+
params.set("filter", this.buildFilter(query));
|
|
389
|
+
params.set("interval.startTime", new Date(startMs).toISOString());
|
|
390
|
+
params.set("interval.endTime", new Date(endMs).toISOString());
|
|
391
|
+
params.set("aggregation.alignmentPeriod", query.alignmentPeriod);
|
|
392
|
+
params.set("aggregation.perSeriesAligner", query.perSeriesAligner);
|
|
393
|
+
params.set("pageSize", String(PAGE_SIZE));
|
|
394
|
+
if (pageToken !== void 0) {
|
|
395
|
+
params.set("pageToken", pageToken);
|
|
396
|
+
}
|
|
397
|
+
const url = `${MONITORING_API_BASE}/projects/${encodeURIComponent(
|
|
398
|
+
this.settings.projectId
|
|
399
|
+
)}/timeSeries?${params.toString()}`;
|
|
400
|
+
const res = await this.get(
|
|
401
|
+
url,
|
|
402
|
+
{
|
|
403
|
+
resource: "time_series",
|
|
404
|
+
headers: {
|
|
405
|
+
Authorization: `Bearer ${accessToken}`,
|
|
406
|
+
"User-Agent": connectorUserAgent(this.id)
|
|
407
|
+
},
|
|
408
|
+
signal
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
return res.body;
|
|
412
|
+
}
|
|
413
|
+
async sync(options, storage, signal) {
|
|
414
|
+
const queries = this.settings.metricQueries;
|
|
415
|
+
if (queries.length === 0) {
|
|
416
|
+
return { done: true };
|
|
417
|
+
}
|
|
418
|
+
const names = new Set(queries.map((q) => q.metricType));
|
|
419
|
+
const { startMs, endMs } = this.computeWindow(options);
|
|
420
|
+
let token = null;
|
|
421
|
+
const getToken = async (sig) => {
|
|
422
|
+
if (token === null) {
|
|
423
|
+
token = await this.getAccessToken(sig);
|
|
424
|
+
}
|
|
425
|
+
return token;
|
|
426
|
+
};
|
|
427
|
+
const samples = [];
|
|
428
|
+
for (const query of queries) {
|
|
429
|
+
let pageToken;
|
|
430
|
+
let page = 0;
|
|
431
|
+
let pageItems = 0;
|
|
432
|
+
let total = 0;
|
|
433
|
+
const phaseStart = Date.now();
|
|
434
|
+
do {
|
|
435
|
+
if (signal?.aborted) {
|
|
436
|
+
return { done: false };
|
|
437
|
+
}
|
|
438
|
+
let response;
|
|
439
|
+
try {
|
|
440
|
+
const accessToken = await getToken(signal);
|
|
441
|
+
response = await this.listTimeSeries(
|
|
442
|
+
accessToken,
|
|
443
|
+
query,
|
|
444
|
+
startMs,
|
|
445
|
+
endMs,
|
|
446
|
+
pageToken,
|
|
447
|
+
signal
|
|
448
|
+
);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
this.logger.warn("fetch page failed", {
|
|
451
|
+
resource: query.metricType,
|
|
452
|
+
page: page + 1,
|
|
453
|
+
queryId: query.id,
|
|
454
|
+
error: err instanceof Error ? err.message : String(err)
|
|
455
|
+
});
|
|
456
|
+
throw err;
|
|
457
|
+
}
|
|
458
|
+
const series = response.timeSeries ?? [];
|
|
459
|
+
pageItems = 0;
|
|
460
|
+
for (const ts of series) {
|
|
461
|
+
for (const point of ts.points ?? []) {
|
|
462
|
+
const sample = pointToSample(query, ts, point);
|
|
463
|
+
if (sample !== null) {
|
|
464
|
+
samples.push(sample);
|
|
465
|
+
pageItems += 1;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
total += pageItems;
|
|
470
|
+
pageToken = typeof response.nextPageToken === "string" && response.nextPageToken.length > 0 ? response.nextPageToken : void 0;
|
|
471
|
+
page += 1;
|
|
472
|
+
this.logger.info("fetched page", {
|
|
473
|
+
resource: query.metricType,
|
|
474
|
+
queryId: query.id,
|
|
475
|
+
page,
|
|
476
|
+
items: pageItems,
|
|
477
|
+
next: pageToken ?? null
|
|
478
|
+
});
|
|
479
|
+
} while (pageToken !== void 0);
|
|
480
|
+
this.logger.info("resource done", {
|
|
481
|
+
resource: query.metricType,
|
|
482
|
+
queryId: query.id,
|
|
483
|
+
pages: page,
|
|
484
|
+
items: total,
|
|
485
|
+
duration_ms: Date.now() - phaseStart
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
await storage.metrics(samples, { names: [...names] });
|
|
489
|
+
return { done: true };
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
function parseDurationSeconds(duration) {
|
|
493
|
+
const m = /^(\d+)s$/.exec(duration);
|
|
494
|
+
if (!m) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const n = Number.parseInt(m[1], 10);
|
|
498
|
+
return Number.isFinite(n) ? n : null;
|
|
499
|
+
}
|
|
500
|
+
function pointToSample(query, series, point) {
|
|
501
|
+
const tsIso = point.interval.endTime;
|
|
502
|
+
const ts = parseEpoch(tsIso, "iso");
|
|
503
|
+
if (ts === null) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
const value = extractScalarValue(point.value);
|
|
507
|
+
if (value === null) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const attributes = {
|
|
511
|
+
perSeriesAligner: query.perSeriesAligner,
|
|
512
|
+
alignmentPeriod: query.alignmentPeriod,
|
|
513
|
+
queryId: query.id
|
|
514
|
+
};
|
|
515
|
+
if (series.resource?.type) {
|
|
516
|
+
attributes["resourceType"] = series.resource.type;
|
|
517
|
+
}
|
|
518
|
+
for (const [k, v] of Object.entries(series.metric.labels ?? {})) {
|
|
519
|
+
attributes[`metric.${k}`] = v;
|
|
520
|
+
}
|
|
521
|
+
for (const [k, v] of Object.entries(series.resource?.labels ?? {})) {
|
|
522
|
+
attributes[`resource.${k}`] = v;
|
|
523
|
+
}
|
|
524
|
+
return { name: query.metricType, ts, value, attributes };
|
|
525
|
+
}
|
|
526
|
+
function extractScalarValue(v) {
|
|
527
|
+
if (v.doubleValue !== void 0 && Number.isFinite(v.doubleValue)) {
|
|
528
|
+
return v.doubleValue;
|
|
529
|
+
}
|
|
530
|
+
if (v.int64Value !== void 0) {
|
|
531
|
+
const n = Number(v.int64Value);
|
|
532
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
533
|
+
}
|
|
534
|
+
if (v.boolValue !== void 0) {
|
|
535
|
+
return v.boolValue ? 1 : 0;
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/index.ts
|
|
541
|
+
var index_default = GcpMonitoringConnector;
|
|
542
|
+
export {
|
|
543
|
+
GcpMonitoringConnector,
|
|
544
|
+
configFields,
|
|
545
|
+
index_default as default,
|
|
546
|
+
doc,
|
|
547
|
+
id,
|
|
548
|
+
parseDurationSeconds,
|
|
549
|
+
pointToSample,
|
|
550
|
+
gcpMonitoringResources as resources
|
|
551
|
+
};
|
|
552
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../gcp-shared/src/auth.ts","../../gcp-shared/src/config.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/gcp-monitoring.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport interface ServiceAccountKey {\n client_email: string;\n private_key: string;\n token_uri?: string;\n project_id?: string;\n}\n\nconst serviceAccountKeySchema = z.object({\n client_email: z.string().min(1),\n private_key: z.string().min(1),\n token_uri: z.string().url().optional(),\n project_id: z.string().optional(),\n});\n\nexport interface TokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport const tokenResponseSchema = z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n});\n\nfunction base64urlFromBytes(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64urlFromString(str: string): string {\n return base64urlFromBytes(new TextEncoder().encode(str));\n}\n\nasync function signRS256JWT(\n payload: Record<string, unknown>,\n privateKeyPem: string,\n): Promise<string> {\n const header = { alg: 'RS256', typ: 'JWT' };\n const headerB64 = base64urlFromString(JSON.stringify(header));\n const payloadB64 = base64urlFromString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const pemContent = privateKeyPem\n .replace(/-----BEGIN PRIVATE KEY-----/g, '')\n .replace(/-----END PRIVATE KEY-----/g, '')\n .replace(/\\s/g, '');\n const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));\n\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n der,\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await globalThis.crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(signingInput),\n );\n\n return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;\n}\n\nexport function parseServiceAccountJson(value: string): ServiceAccountKey {\n const trimmed = value.trim();\n if (trimmed.startsWith('{')) {\n return serviceAccountKeySchema.parse(JSON.parse(trimmed));\n }\n const binary = atob(trimmed);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const decoded = new TextDecoder().decode(bytes);\n return serviceAccountKeySchema.parse(JSON.parse(decoded));\n}\n\n/**\n * Build the OAuth 2.0 token-exchange request body for a Google service account.\n * The caller posts the returned body to the returned url and parses the\n * resulting `access_token` (see {@link tokenResponseSchema}).\n *\n * Scope is a space-delimited list of OAuth scope URLs (e.g.\n * `https://www.googleapis.com/auth/monitoring.read`).\n */\nexport async function buildServiceAccountJwt(\n serviceAccountJson: string,\n scope: string,\n): Promise<{ url: string; body: string }> {\n const sa = parseServiceAccountJson(serviceAccountJson);\n const now = Math.floor(Date.now() / 1000);\n const jwt = await signRS256JWT(\n {\n iss: sa.client_email,\n scope,\n aud: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now,\n },\n sa.private_key,\n );\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }).toString();\n\n return {\n url: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n body,\n };\n}\n","import { z } from 'zod';\n\n/**\n * Shared GCP config fragment - reused by both `@rawdash/connector-gcp-monitoring`\n * and `@rawdash/connector-gcp-billing`. Both authenticate with a service-account\n * JSON key; the per-connector schema spreads this in alongside its own fields.\n */\nexport const gcpAuthConfigShape = {\n serviceAccountJson: z.object({ $secret: z.string().trim().min(1) }).meta({\n label: 'Service Account JSON',\n description:\n 'Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.',\n secret: true,\n }),\n} as const;\n\nexport interface GcpAuthConfig {\n serviceAccountJson: { $secret: string };\n}\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n buildServiceAccountJwt,\n gcpAuthConfigShape,\n tokenResponseSchema,\n} from '@rawdash/connector-gcp-shared';\nimport {\n AuthError,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n schemasFromResources,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst metricQuerySchema = z.object({\n id: z\n .string()\n .regex(\n /^[a-z][a-zA-Z0-9_]*$/,\n 'Monitoring query id must start with a lowercase letter and contain only letters, digits, and underscores',\n ),\n metricType: z.string().min(1).meta({\n description:\n 'Fully-qualified Cloud Monitoring metric type, e.g. compute.googleapis.com/instance/cpu/utilization.',\n }),\n filter: z.string().optional().meta({\n description:\n 'Optional additional filter combined with metric.type using AND, e.g. resource.labels.zone=\"us-central1-a\".',\n }),\n alignmentPeriod: z\n .string()\n .regex(\n /^\\d+s$/,\n 'alignmentPeriod must be a duration in seconds, e.g. 60s or 300s',\n )\n .meta({\n description:\n 'Aggregation alignment period as a duration in seconds, e.g. 300s.',\n }),\n perSeriesAligner: z\n .enum([\n 'ALIGN_NONE',\n 'ALIGN_DELTA',\n 'ALIGN_RATE',\n 'ALIGN_INTERPOLATE',\n 'ALIGN_NEXT_OLDER',\n 'ALIGN_MIN',\n 'ALIGN_MAX',\n 'ALIGN_MEAN',\n 'ALIGN_COUNT',\n 'ALIGN_SUM',\n 'ALIGN_STDDEV',\n 'ALIGN_COUNT_TRUE',\n 'ALIGN_COUNT_FALSE',\n 'ALIGN_FRACTION_TRUE',\n 'ALIGN_PERCENTILE_99',\n 'ALIGN_PERCENTILE_95',\n 'ALIGN_PERCENTILE_50',\n 'ALIGN_PERCENTILE_05',\n 'ALIGN_PERCENT_CHANGE',\n ])\n .meta({\n description:\n 'Cloud Monitoring perSeriesAligner statistic, e.g. ALIGN_MEAN, ALIGN_SUM, ALIGN_PERCENTILE_99.',\n }),\n});\n\nexport const configFields = defineConfigFields(\n z\n .object({\n projectId: z.string().min(1).meta({\n label: 'GCP project ID',\n description:\n 'Google Cloud project ID whose metrics should be synced (the project that owns the monitored resources).',\n placeholder: 'my-project-123',\n }),\n ...gcpAuthConfigShape,\n metricQueries: z.array(metricQuerySchema).nonempty().meta({\n label: 'Metric queries',\n description:\n 'Cloud Monitoring is too broad to mirror wholesale; declare the specific metrics to pull. Each query needs an id, metric type, alignment period (e.g. 300s), and a perSeriesAligner statistic, with an optional filter on resource labels.',\n }),\n lookbackMinutes: z.number().int().positive().max(40_320).optional().meta({\n label: 'Lookback (minutes)',\n description:\n 'How far back to pull data points on a full sync when the host does not supply a since bound. Defaults to 180.',\n placeholder: '180',\n }),\n })\n .refine(\n (cfg) =>\n new Set(cfg.metricQueries.map((q) => q.id)).size ===\n cfg.metricQueries.length,\n {\n path: ['metricQueries'],\n message: 'Each metric query id must be unique',\n },\n ),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Google Cloud Monitoring',\n category: 'infrastructure',\n brandColor: '#4285F4',\n tagline:\n 'Pull declared Cloud Monitoring metric time series (any metric type, aligner, and period) into a single metric series per query.',\n rateLimit:\n 'Cloud Monitoring projects.timeSeries.list is rate-limited per project; 429 / RESOURCE_EXHAUSTED responses are retried with backoff. Pagination uses nextPageToken.',\n vendor: {\n name: 'Google Cloud',\n apiDocs: 'https://cloud.google.com/monitoring/api/v3',\n website: 'https://cloud.google.com/monitoring',\n },\n auth: {\n summary:\n 'Authenticate against the Cloud Monitoring v3 API with a Google service account JSON key. The service account needs the Monitoring Viewer role (roles/monitoring.viewer) on the project whose metrics it reads.',\n setup: [\n 'Identify the GCP project whose metrics you want to sync.',\n 'Create a service account at Google Cloud -> IAM & Admin -> Service Accounts in that project (or grant an existing one access).',\n 'Grant the service account the Monitoring Viewer role (roles/monitoring.viewer) on the project. The API enables this role automatically for owners and editors.',\n 'Generate a JSON key for the service account and store its contents as a secret (e.g. GCP_MONITORING_SA_JSON).',\n 'Reference the key from config as serviceAccountJson: secret(\"GCP_MONITORING_SA_JSON\") and set projectId to the same project.',\n ],\n },\n limitations: [\n 'Cloud Monitoring is too broad to mirror wholesale; only the metrics declared in metricQueries are synced; there is no automatic metric discovery.',\n 'The series name is derived from the metric type, so two queries against the same metricType with different aligners or filters share one series name and are distinguished only by sample attributes.',\n 'Each query alignmentPeriod must be expressed as a duration in seconds, e.g. 60s or 300s.',\n 'A full sync uses lookbackMinutes; a latest sync uses a short window covering the last few alignment periods.',\n 'Distribution-valued metrics (e.g. latency histograms) require a perSeriesAligner that reduces them to a scalar (ALIGN_PERCENTILE_99, ALIGN_MEAN, etc.); raw distributions are not stored.',\n ],\n});\n\n// ---------------------------------------------------------------------------\n// Settings / credentials\n// ---------------------------------------------------------------------------\n\nexport interface GcpMonitoringMetricQuery {\n id: string;\n metricType: string;\n filter?: string;\n alignmentPeriod: string;\n perSeriesAligner: string;\n}\n\nexport interface GcpMonitoringSettings {\n projectId: string;\n metricQueries: GcpMonitoringMetricQuery[];\n lookbackMinutes?: number;\n}\n\nconst gcpMonitoringCredentials = {\n serviceAccountJson: {\n description: 'Google service account JSON key (raw JSON or base64)',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype GcpMonitoringCredentials = typeof gcpMonitoringCredentials;\n\n// ---------------------------------------------------------------------------\n// Cloud Monitoring v3 response schemas\n// ---------------------------------------------------------------------------\n\nconst int64String = z.string().regex(/^-?\\d+$/);\nconst isoTimestamp = z.iso.datetime();\n\nconst pointValue = z\n .object({\n doubleValue: z.number().optional(),\n int64Value: int64String.optional(),\n boolValue: z.boolean().optional(),\n stringValue: z.string().optional(),\n distributionValue: z.unknown().optional(),\n })\n .refine(\n (v) =>\n v.doubleValue !== undefined ||\n v.int64Value !== undefined ||\n v.boolValue !== undefined ||\n v.stringValue !== undefined ||\n v.distributionValue !== undefined,\n {\n message: 'point value must carry at least one supported value field',\n },\n );\n\nconst pointSchema = z.object({\n interval: z.object({\n startTime: isoTimestamp.optional(),\n endTime: isoTimestamp,\n }),\n value: pointValue,\n});\n\nconst timeSeriesSchema = z.object({\n metric: z.object({\n type: z.string(),\n labels: z.record(z.string(), z.string()).optional(),\n }),\n resource: z\n .object({\n type: z.string().optional(),\n labels: z.record(z.string(), z.string()).optional(),\n })\n .optional(),\n valueType: z.string().optional(),\n metricKind: z.string().optional(),\n points: z.array(pointSchema).optional(),\n});\n\nconst listTimeSeriesResponseSchema = z.object({\n timeSeries: z.array(timeSeriesSchema).optional(),\n nextPageToken: z.string().optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Resources\n// ---------------------------------------------------------------------------\n\nexport const gcpMonitoringResources = defineResources({\n '<metricType>': {\n shape: 'metric',\n dynamic: true,\n description:\n 'One metric series per declared metric query. The series name is the configured metric type (e.g. `compute.googleapis.com/instance/cpu/utilization`), so the actual keys depend on the configured `metricQueries`. Each sample carries the aligner, alignment period, query id, and metric/resource labels as attributes.',\n endpoint: 'GET /v3/projects/{projectId}/timeSeries',\n granularity: 'Per alignmentPeriod (a duration in seconds, e.g. 300s)',\n notes:\n 'Each sync replaces the full set of samples for the metric names it owns (idempotent). Distribution-valued points are dropped unless reduced to a scalar by the perSeriesAligner.',\n dimensions: [\n {\n name: 'perSeriesAligner',\n description:\n 'The Cloud Monitoring statistic requested for the query, e.g. ALIGN_MEAN, ALIGN_SUM, or ALIGN_PERCENTILE_99.',\n },\n {\n name: 'alignmentPeriod',\n description:\n 'The aggregation alignment period as configured, e.g. 300s.',\n },\n {\n name: 'queryId',\n description:\n 'The configured id of the metric query that produced the sample.',\n },\n {\n name: 'resourceType',\n description:\n 'The monitored resource type the sample originated from (e.g. gce_instance).',\n },\n ],\n responses: {\n oauth_token: tokenResponseSchema,\n time_series: listTimeSeriesResponseSchema,\n },\n },\n});\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MONITORING_API_BASE = 'https://monitoring.googleapis.com/v3';\nconst MONITORING_SCOPE = 'https://www.googleapis.com/auth/monitoring.read';\nconst PAGE_SIZE = 1000;\nconst DEFAULT_LOOKBACK_MINUTES = 180;\nconst MS_PER_MINUTE = 60_000;\n\nexport const id = 'gcp-monitoring';\n\n// ---------------------------------------------------------------------------\n// GcpMonitoringConnector\n// ---------------------------------------------------------------------------\n\nexport class GcpMonitoringConnector extends BaseConnector<\n GcpMonitoringSettings,\n GcpMonitoringCredentials\n> {\n static readonly id = id;\n\n static readonly resources = gcpMonitoringResources;\n\n static readonly schemas = schemasFromResources(gcpMonitoringResources);\n\n static create(\n input: unknown,\n ctx?: ConnectorContext,\n ): GcpMonitoringConnector {\n const parsed = configFields.parse(input);\n return new GcpMonitoringConnector(\n {\n projectId: parsed.projectId,\n metricQueries: parsed.metricQueries,\n lookbackMinutes: parsed.lookbackMinutes,\n },\n {\n serviceAccountJson: parsed.serviceAccountJson,\n },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = gcpMonitoringCredentials;\n\n private cachedToken: { token: string; expiresAt: number } | null = null;\n\n private async getAccessToken(signal?: AbortSignal): Promise<string> {\n if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {\n return this.cachedToken.token;\n }\n const { serviceAccountJson } = this.creds;\n if (!serviceAccountJson) {\n throw new AuthError(`${this.id}: missing serviceAccountJson credential`);\n }\n const { url, body } = await buildServiceAccountJwt(\n serviceAccountJson,\n MONITORING_SCOPE,\n );\n const res = await this.post<{\n access_token: string;\n expires_in?: number;\n }>(url, {\n resource: 'oauth_token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n signal,\n });\n const expiresIn = res.body.expires_in ?? 3600;\n this.cachedToken = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cachedToken.token;\n }\n\n private computeWindow(options: SyncOptions): {\n startMs: number;\n endMs: number;\n } {\n const endMs = Date.now();\n if (options.since) {\n const sinceMs = parseEpoch(options.since, 'iso');\n if (sinceMs !== null) {\n return { startMs: Math.min(sinceMs, endMs), endMs };\n }\n }\n if (options.mode === 'latest') {\n const maxPeriodSec = Math.max(\n ...this.settings.metricQueries.map(\n (q) => parseDurationSeconds(q.alignmentPeriod) ?? 60,\n ),\n 60,\n );\n return { startMs: endMs - maxPeriodSec * 3 * 1000, endMs };\n }\n const lookback = this.settings.lookbackMinutes ?? DEFAULT_LOOKBACK_MINUTES;\n return { startMs: endMs - lookback * MS_PER_MINUTE, endMs };\n }\n\n private buildFilter(query: GcpMonitoringMetricQuery): string {\n const base = `metric.type = \"${query.metricType}\"`;\n if (query.filter && query.filter.trim().length > 0) {\n return `${base} AND ${query.filter}`;\n }\n return base;\n }\n\n private async listTimeSeries(\n accessToken: string,\n query: GcpMonitoringMetricQuery,\n startMs: number,\n endMs: number,\n pageToken: string | undefined,\n signal?: AbortSignal,\n ): Promise<z.infer<typeof listTimeSeriesResponseSchema>> {\n const params = new URLSearchParams();\n params.set('filter', this.buildFilter(query));\n params.set('interval.startTime', new Date(startMs).toISOString());\n params.set('interval.endTime', new Date(endMs).toISOString());\n params.set('aggregation.alignmentPeriod', query.alignmentPeriod);\n params.set('aggregation.perSeriesAligner', query.perSeriesAligner);\n params.set('pageSize', String(PAGE_SIZE));\n if (pageToken !== undefined) {\n params.set('pageToken', pageToken);\n }\n\n const url = `${MONITORING_API_BASE}/projects/${encodeURIComponent(\n this.settings.projectId,\n )}/timeSeries?${params.toString()}`;\n\n const res = await this.get<z.infer<typeof listTimeSeriesResponseSchema>>(\n url,\n {\n resource: 'time_series',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'User-Agent': connectorUserAgent(this.id),\n },\n signal,\n },\n );\n return res.body;\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const queries = this.settings.metricQueries;\n if (queries.length === 0) {\n return { done: true };\n }\n\n const names = new Set(queries.map((q) => q.metricType));\n const { startMs, endMs } = this.computeWindow(options);\n\n let token: string | null = null;\n const getToken = async (sig?: AbortSignal): Promise<string> => {\n if (token === null) {\n token = await this.getAccessToken(sig);\n }\n return token;\n };\n\n const samples: MetricSample[] = [];\n\n for (const query of queries) {\n let pageToken: string | undefined;\n let page = 0;\n let pageItems = 0;\n let total = 0;\n const phaseStart = Date.now();\n do {\n if (signal?.aborted) {\n return { done: false };\n }\n let response: z.infer<typeof listTimeSeriesResponseSchema>;\n try {\n const accessToken = await getToken(signal);\n response = await this.listTimeSeries(\n accessToken,\n query,\n startMs,\n endMs,\n pageToken,\n signal,\n );\n } catch (err) {\n this.logger.warn('fetch page failed', {\n resource: query.metricType,\n page: page + 1,\n queryId: query.id,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n const series = response.timeSeries ?? [];\n pageItems = 0;\n for (const ts of series) {\n for (const point of ts.points ?? []) {\n const sample = pointToSample(query, ts, point);\n if (sample !== null) {\n samples.push(sample);\n pageItems += 1;\n }\n }\n }\n total += pageItems;\n pageToken =\n typeof response.nextPageToken === 'string' &&\n response.nextPageToken.length > 0\n ? response.nextPageToken\n : undefined;\n page += 1;\n this.logger.info('fetched page', {\n resource: query.metricType,\n queryId: query.id,\n page,\n items: pageItems,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n this.logger.info('resource done', {\n resource: query.metricType,\n queryId: query.id,\n pages: page,\n items: total,\n duration_ms: Date.now() - phaseStart,\n });\n }\n\n await storage.metrics(samples, { names: [...names] });\n return { done: true };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers (exported for tests)\n// ---------------------------------------------------------------------------\n\nexport function parseDurationSeconds(duration: string): number | null {\n const m = /^(\\d+)s$/.exec(duration);\n if (!m) {\n return null;\n }\n const n = Number.parseInt(m[1]!, 10);\n return Number.isFinite(n) ? n : null;\n}\n\nexport function pointToSample(\n query: GcpMonitoringMetricQuery,\n series: z.infer<typeof timeSeriesSchema>,\n point: z.infer<typeof pointSchema>,\n): MetricSample | null {\n const tsIso = point.interval.endTime;\n const ts = parseEpoch(tsIso, 'iso');\n if (ts === null) {\n return null;\n }\n const value = extractScalarValue(point.value);\n if (value === null) {\n return null;\n }\n const attributes: Record<string, JSONValue> = {\n perSeriesAligner: query.perSeriesAligner,\n alignmentPeriod: query.alignmentPeriod,\n queryId: query.id,\n };\n if (series.resource?.type) {\n attributes['resourceType'] = series.resource.type;\n }\n for (const [k, v] of Object.entries(series.metric.labels ?? {})) {\n attributes[`metric.${k}`] = v;\n }\n for (const [k, v] of Object.entries(series.resource?.labels ?? {})) {\n attributes[`resource.${k}`] = v;\n }\n return { name: query.metricType, ts, value, attributes };\n}\n\nfunction extractScalarValue(v: z.infer<typeof pointValue>): number | null {\n if (v.doubleValue !== undefined && Number.isFinite(v.doubleValue)) {\n return v.doubleValue;\n }\n if (v.int64Value !== undefined) {\n const n = Number(v.int64Value);\n return Number.isSafeInteger(n) ? n : null;\n }\n if (v.boolValue !== undefined) {\n return v.boolValue ? 1 : 0;\n }\n // stringValue and distributionValue are not scalar; drop them silently. The\n // resource doc calls this out explicitly so users know to pick an aligner\n // that reduces distributions (e.g. ALIGN_PERCENTILE_99) when needed.\n return null;\n}\n","import { GcpMonitoringConnector } from './gcp-monitoring';\n\nexport {\n GcpMonitoringConnector,\n configFields,\n doc,\n id,\n parseDurationSeconds,\n pointToSample,\n gcpMonitoringResources as resources,\n} from './gcp-monitoring';\nexport type {\n GcpMonitoringMetricQuery,\n GcpMonitoringSettings,\n} from './gcp-monitoring';\nexport default GcpMonitoringConnector;\n"],"mappings":"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","names":["z","z"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rawdash/connector-gcp-monitoring",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Rawdash connector for Google Cloud Monitoring — syncs declared metric time series via the Cloud Monitoring v3 API into the six-shape storage model",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/rawdash/rawdash.git",
|
|
11
|
+
"directory": "packages/connectors/gcp-monitoring"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"@rawdash/source": "./src/index.ts",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"lint": "eslint src",
|
|
29
|
+
"test": "vitest run"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@rawdash/core": "workspace:*",
|
|
33
|
+
"zod": "^4.4.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@rawdash/connector-gcp-shared": "workspace:*",
|
|
37
|
+
"@rawdash/connector-shared": "workspace:*",
|
|
38
|
+
"@rawdash/connector-test-utils": "workspace:*",
|
|
39
|
+
"fast-check": "^4.8.0",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5.7.2",
|
|
42
|
+
"vitest": "^4.1.4"
|
|
43
|
+
}
|
|
44
|
+
}
|