@causa/workspace-google 1.4.0-beta.1 → 1.4.0-beta.2

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.
Files changed (56) hide show
  1. package/README.md +14 -0
  2. package/dist/functions/google-app-check/generate-token.js +3 -1
  3. package/dist/functions/google-firebase-storage/merge-rules.js +3 -1
  4. package/dist/functions/google-firestore/merge-rules.js +3 -1
  5. package/dist/functions/google-identity-platform/generate-custom-token.js +7 -2
  6. package/dist/functions/google-pubsub/write-topics.js +7 -2
  7. package/dist/functions/google-services/enable.js +3 -1
  8. package/dist/functions/google-spanner/write-databases.js +7 -2
  9. package/dist/functions/graph/fetch-environment-metric-series.call.d.ts +3 -0
  10. package/dist/functions/graph/fetch-environment-metric-series.call.js +107 -0
  11. package/dist/functions/graph/fetch-environment-metric-series.d.ts +23 -0
  12. package/dist/functions/graph/fetch-environment-metric-series.js +40 -0
  13. package/dist/functions/graph/get-environment-provider.d.ts +11 -0
  14. package/dist/functions/graph/get-environment-provider.js +20 -0
  15. package/dist/functions/graph/index.d.ts +2 -0
  16. package/dist/functions/graph/index.js +2 -0
  17. package/dist/functions/index.js +2 -2
  18. package/dist/graph/cloud-run.d.ts +2 -2
  19. package/dist/graph/cloud-run.js +2 -2
  20. package/dist/graph/environment/fetchers/cloud-monitoring-alerts.d.ts +6 -0
  21. package/dist/graph/environment/fetchers/cloud-monitoring-alerts.js +106 -0
  22. package/dist/graph/environment/fetchers/cloud-monitoring.d.ts +5 -0
  23. package/dist/graph/environment/fetchers/cloud-monitoring.js +13 -0
  24. package/dist/graph/environment/fetchers/cloud-run.d.ts +5 -0
  25. package/dist/graph/environment/fetchers/cloud-run.js +88 -0
  26. package/dist/graph/environment/fetchers/cloud-scheduler.d.ts +5 -0
  27. package/dist/graph/environment/fetchers/cloud-scheduler.js +95 -0
  28. package/dist/graph/environment/fetchers/console-links.d.ts +14 -0
  29. package/dist/graph/environment/fetchers/console-links.js +132 -0
  30. package/dist/graph/environment/fetchers/error-reporting.d.ts +5 -0
  31. package/dist/graph/environment/fetchers/error-reporting.js +98 -0
  32. package/dist/graph/environment/fetchers/index.d.ts +5 -0
  33. package/dist/graph/environment/fetchers/index.js +17 -0
  34. package/dist/graph/environment/index.d.ts +9 -0
  35. package/dist/graph/environment/index.js +9 -0
  36. package/dist/graph/environment/load-balancer-backends.d.ts +82 -0
  37. package/dist/graph/environment/load-balancer-backends.js +126 -0
  38. package/dist/graph/environment/monitoring-alerts.d.ts +60 -0
  39. package/dist/graph/environment/monitoring-alerts.js +67 -0
  40. package/dist/graph/environment/monitoring-catalog.d.ts +95 -0
  41. package/dist/graph/environment/monitoring-catalog.js +356 -0
  42. package/dist/graph/environment/monitoring-mappings.d.ts +133 -0
  43. package/dist/graph/environment/monitoring-mappings.js +97 -0
  44. package/dist/graph/environment/monitoring-query.d.ts +116 -0
  45. package/dist/graph/environment/monitoring-query.js +314 -0
  46. package/dist/graph/environment/monitoring-values.d.ts +41 -0
  47. package/dist/graph/environment/monitoring-values.js +119 -0
  48. package/dist/graph/environment/resource-resolvers.d.ts +7 -0
  49. package/dist/graph/environment/resource-resolvers.js +57 -0
  50. package/dist/graph/environment/resources.d.ts +45 -0
  51. package/dist/graph/environment/resources.js +49 -0
  52. package/dist/graph/index.d.ts +1 -0
  53. package/dist/graph/index.js +1 -0
  54. package/dist/graph/spanner.d.ts +2 -2
  55. package/dist/graph/spanner.js +2 -2
  56. package/package.json +19 -19
package/README.md CHANGED
@@ -83,6 +83,20 @@ This module contributes to the architecture graph extracted by `cs graph extract
83
83
 
84
84
  Infrastructure nodes carry the Google Cloud resource they stand for in `data.resource`: its `type` (e.g. `pubsub.googleapis.com/Topic`), its `id`, i.e. its full resource name (or `idPrefix` when the full name is not known), and the `scope`, i.e. the project whose configuration should be used to render the identifier.
85
85
 
86
+ When the graph is enriched with environment data, this module resolves the names of Cloud Tasks queues by listing the queues of the environment. Queues that cannot be resolved are removed from their nodes. This module's fetchers then:
87
+
88
+ - read metrics of services, API routers, Pub/Sub topics and subscriptions, Cloud Tasks queues, and Spanner, Firestore, and BigQuery databases from Cloud Monitoring
89
+ - add the `deployment` of Cloud Run services, and the `lastAttempt` and `nextRun` of Cloud Scheduler jobs. They describe services and jobs as they are when the graph is enriched, with a warning when they changed after `at`.
90
+ - add `links` to the Google Cloud console for each resource
91
+ - attach the alerts open in Cloud Monitoring to the nodes of their resources. Matching an alert to a node requires the condition of its policy to keep the labels making up the resource name (e.g. `project_id`, `location`, and `service_name`), along with `monitored_resource` when the metric is written against several resource types, like `logging.googleapis.com/log_entry_count`.
92
+ - attach the open Error Reporting groups of Cloud Run services to their nodes, as alerts with the `error` severity. A group is open when it has occurrences during the last 30 days, and is not acknowledged, resolved, or muted.
93
+
94
+ The requests and latencies of API routers are also grouped by the `node` the requests were routed to, by mapping the serverless network endpoint groups of their load balancers to Cloud Run services. They are listed using the Compute Engine API (`compute.networkEndpointGroups.list`) in the projects of the API routers.
95
+
96
+ `GraphFetchEnvironmentMetricSeries` is implemented for the metrics read from Cloud Monitoring.
97
+
98
+ Cloud Monitoring exposes complete data with a delay of a few minutes. Metrics, including their time series, are read over windows ending at most 5 minutes ago.
99
+
86
100
  ### Secrets backend
87
101
 
88
102
  This module implements the `google.secretManager` secret backend, allowing fetching secrets from the Google Secret Manager service. Here are some example of how secrets with the `google.secretManager` backend should be defined:
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { CliCommand, CliOption } from '@causa/cli';
11
11
  import { callDeferred, WorkspaceFunction } from '@causa/workspace';
12
+ import { PassArgumentsByReference } from '@causa/workspace/function-registry';
12
13
  import { AllowMissing } from '@causa/workspace/validation';
13
14
  import { IsString } from 'class-validator';
14
15
  import { appCheckCommandDefinition } from '../../cli/index.js';
@@ -48,6 +49,7 @@ If the Firebase app ID is not specified, it will:
48
49
  - If not present in the configuration, will be automatically looked up from the API.`,
49
50
  summary: 'Generates an AppCheck token.',
50
51
  outputFn: (token) => console.log(token),
51
- })
52
+ }),
53
+ PassArgumentsByReference()
52
54
  ], GoogleAppCheckGenerateToken);
53
55
  export { GoogleAppCheckGenerateToken };
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { CliCommand } from '@causa/cli';
11
11
  import { WorkspaceFunction } from '@causa/workspace';
12
+ import { PassArgumentsByReference } from '@causa/workspace/function-registry';
12
13
  import { AllowMissing } from '@causa/workspace/validation';
13
14
  import { IsBoolean } from 'class-validator';
14
15
  import { firebaseStorageCommandDefinition } from '../../cli/index.js';
@@ -67,6 +68,7 @@ GoogleFirebaseStorageMergeRules = __decorate([
67
68
  Input files are looked for in the workspace using the globs defined in google.firebaseStorage.securityRulesFiles.`,
68
69
  summary: 'Merge Firebase Storage security rules into a single file.',
69
70
  outputFn: ({ securityRuleFile }) => console.log(securityRuleFile),
70
- })
71
+ }),
72
+ PassArgumentsByReference()
71
73
  ], GoogleFirebaseStorageMergeRules);
72
74
  export { GoogleFirebaseStorageMergeRules };
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { CliCommand } from '@causa/cli';
11
11
  import { WorkspaceFunction } from '@causa/workspace';
12
+ import { PassArgumentsByReference } from '@causa/workspace/function-registry';
12
13
  import { AllowMissing } from '@causa/workspace/validation';
13
14
  import { IsBoolean } from 'class-validator';
14
15
  import { firestoreCommandDefinition } from '../../cli/index.js';
@@ -67,6 +68,7 @@ GoogleFirestoreMergeRules = __decorate([
67
68
  Input files are looked for in the workspace using the globs defined in google.firestore.securityRulesFiles.`,
68
69
  summary: 'Merge Firestore security rules into a single file.',
69
70
  outputFn: ({ securityRuleFile }) => console.log(securityRuleFile),
70
- })
71
+ }),
72
+ PassArgumentsByReference()
71
73
  ], GoogleFirestoreMergeRules);
72
74
  export { GoogleFirestoreMergeRules };
@@ -8,6 +8,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
10
  import { callDeferred, WorkspaceFunction } from '@causa/workspace';
11
+ import { PassArgumentsByReference } from '@causa/workspace/function-registry';
11
12
  import { AllowMissing } from '@causa/workspace/validation';
12
13
  import { IsObject, IsString } from 'class-validator';
13
14
  /**
@@ -17,7 +18,7 @@ import { IsObject, IsString } from 'class-validator';
17
18
  * If `google.firebase.adminServiceAccount` is set, the service account will be used to sign the token. Otherwise, an
18
19
  * attempt will be made to find a valid service account in the `google.project`.
19
20
  */
20
- export class GoogleIdentityPlatformGenerateCustomToken extends WorkspaceFunction {
21
+ let GoogleIdentityPlatformGenerateCustomToken = class GoogleIdentityPlatformGenerateCustomToken extends WorkspaceFunction {
21
22
  /**
22
23
  * The ID of the user for which the custom token should be generated.
23
24
  */
@@ -32,7 +33,7 @@ export class GoogleIdentityPlatformGenerateCustomToken extends WorkspaceFunction
32
33
  _supports() {
33
34
  return true;
34
35
  }
35
- }
36
+ };
36
37
  __decorate([
37
38
  IsString(),
38
39
  __metadata("design:type", String)
@@ -42,3 +43,7 @@ __decorate([
42
43
  AllowMissing(),
43
44
  __metadata("design:type", Object)
44
45
  ], GoogleIdentityPlatformGenerateCustomToken.prototype, "claims", void 0);
46
+ GoogleIdentityPlatformGenerateCustomToken = __decorate([
47
+ PassArgumentsByReference()
48
+ ], GoogleIdentityPlatformGenerateCustomToken);
49
+ export { GoogleIdentityPlatformGenerateCustomToken };
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { WorkspaceFunction } from '@causa/workspace';
11
11
  import { EventTopicList, } from '@causa/workspace-core';
12
+ import { PassArgumentsByReference } from '@causa/workspace/function-registry';
12
13
  import { CAUSA_FOLDER } from '@causa/workspace/initialization';
13
14
  import { AllowMissing } from '@causa/workspace/validation';
14
15
  import { IsBoolean } from 'class-validator';
@@ -34,7 +35,7 @@ export function rawEventsTableName(topic) {
34
35
  * topic configurations.
35
36
  * This function returns a partial configuration, such that it can be used as a processor.
36
37
  */
37
- export class GooglePubSubWriteTopics extends WorkspaceFunction {
38
+ let GooglePubSubWriteTopics = class GooglePubSubWriteTopics extends WorkspaceFunction {
38
39
  tearDown;
39
40
  /**
40
41
  * Returns the path to the directory where Pub/Sub topic configurations should be written.
@@ -80,9 +81,13 @@ export class GooglePubSubWriteTopics extends WorkspaceFunction {
80
81
  _supports() {
81
82
  return true;
82
83
  }
83
- }
84
+ };
84
85
  __decorate([
85
86
  IsBoolean(),
86
87
  AllowMissing(),
87
88
  __metadata("design:type", Boolean)
88
89
  ], GooglePubSubWriteTopics.prototype, "tearDown", void 0);
90
+ GooglePubSubWriteTopics = __decorate([
91
+ PassArgumentsByReference()
92
+ ], GooglePubSubWriteTopics);
93
+ export { GooglePubSubWriteTopics };
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { CliCommand } from '@causa/cli';
11
11
  import { callDeferred, WorkspaceFunction } from '@causa/workspace';
12
+ import { PassArgumentsByReference } from '@causa/workspace/function-registry';
12
13
  import { AllowMissing } from '@causa/workspace/validation';
13
14
  import { IsBoolean } from 'class-validator';
14
15
  import { googleCommandDefinition } from '../../cli/index.js';
@@ -38,6 +39,7 @@ Required GCP services should be defined in the 'google.services' configuration.
38
39
  They will be enabled in the 'google.project' GCP project.`,
39
40
  summary: 'Enables the GCP services used by the current project.',
40
41
  outputFn: ({ services }) => console.log(services.join('\n')),
41
- })
42
+ }),
43
+ PassArgumentsByReference()
42
44
  ], GoogleServicesEnable);
43
45
  export { GoogleServicesEnable };
@@ -8,6 +8,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
10
  import { WorkspaceFunction } from '@causa/workspace';
11
+ import { PassArgumentsByReference } from '@causa/workspace/function-registry';
11
12
  import { CAUSA_FOLDER } from '@causa/workspace/initialization';
12
13
  import { AllowMissing } from '@causa/workspace/validation';
13
14
  import { IsBoolean } from 'class-validator';
@@ -25,7 +26,7 @@ const DEFAULT_DATABASE_CONFIGURATIONS_DIRECTORY = join(CAUSA_FOLDER, 'spanner-da
25
26
  * database configurations.
26
27
  * This function returns a partial configuration, such that it can be used as a processor.
27
28
  */
28
- export class GoogleSpannerWriteDatabases extends WorkspaceFunction {
29
+ let GoogleSpannerWriteDatabases = class GoogleSpannerWriteDatabases extends WorkspaceFunction {
29
30
  tearDown;
30
31
  /**
31
32
  * Returns the path to the directory where Spanner databases configurations should be written.
@@ -64,9 +65,13 @@ export class GoogleSpannerWriteDatabases extends WorkspaceFunction {
64
65
  _supports() {
65
66
  return true;
66
67
  }
67
- }
68
+ };
68
69
  __decorate([
69
70
  IsBoolean(),
70
71
  AllowMissing(),
71
72
  __metadata("design:type", Boolean)
72
73
  ], GoogleSpannerWriteDatabases.prototype, "tearDown", void 0);
74
+ GoogleSpannerWriteDatabases = __decorate([
75
+ PassArgumentsByReference()
76
+ ], GoogleSpannerWriteDatabases);
77
+ export { GoogleSpannerWriteDatabases };
@@ -0,0 +1,3 @@
1
+ import { type GraphMetricSeries } from '@causa/workspace-core';
2
+ import type { GraphFetchEnvironmentMetricSeriesForGoogle } from './fetch-environment-metric-series.js';
3
+ export default function call(this: GraphFetchEnvironmentMetricSeriesForGoogle): Promise<GraphMetricSeries>;
@@ -0,0 +1,107 @@
1
+ import { GraphMetricKind, } from '@causa/workspace-core';
2
+ import { GraphEnvironmentContext } from '@causa/workspace-core/graph';
3
+ import { monitoringMetricKind } from '../../graph/environment/monitoring-catalog.js';
4
+ import { MIN_ALIGNMENT_PERIOD, monitoringEnd, queryMonitoringMetric, } from '../../graph/environment/monitoring-query.js';
5
+ import { BucketLayoutRegistry } from '../../graph/environment/monitoring-values.js';
6
+ /**
7
+ * The steps between points from which the default one is chosen, in seconds.
8
+ */
9
+ const DEFAULT_STEPS = [
10
+ 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 21600, 43200, 86400,
11
+ ];
12
+ /**
13
+ * The maximum number of points in a series when the step is chosen by default.
14
+ */
15
+ const DEFAULT_MAX_POINTS = 300;
16
+ /**
17
+ * Returns the default step for a series, as the smallest of {@link DEFAULT_STEPS} that results in at most
18
+ * {@link DEFAULT_MAX_POINTS} points. For longer series, a whole number of days is used.
19
+ *
20
+ * @param duration The duration of the series, in seconds.
21
+ * @returns The step, in seconds.
22
+ */
23
+ function defaultStep(duration) {
24
+ const minimum = duration / DEFAULT_MAX_POINTS;
25
+ const day = DEFAULT_STEPS[DEFAULT_STEPS.length - 1];
26
+ return (DEFAULT_STEPS.find((step) => step >= minimum) ??
27
+ Math.ceil(minimum / day) * day);
28
+ }
29
+ /**
30
+ * The maximum number of points in a series.
31
+ */
32
+ const MAX_POINTS = 10000;
33
+ export default async function call() {
34
+ const metric = this.findMetric();
35
+ if (!metric) {
36
+ throw new Error(`The metric '${this.metric}' of node '${this.node}' is not read from Cloud Monitoring.`);
37
+ }
38
+ const { start: requestedStart } = this;
39
+ if (isNaN(this.end.getTime()) || isNaN(requestedStart.getTime())) {
40
+ throw new Error('The start and end of the series must be valid dates.');
41
+ }
42
+ const end = monitoringEnd(this.end);
43
+ const duration = (end.getTime() - requestedStart.getTime()) / 1000;
44
+ const requestedStep = this.step ?? defaultStep(duration);
45
+ if (!Number.isInteger(requestedStep) ||
46
+ requestedStep < MIN_ALIGNMENT_PERIOD) {
47
+ throw new Error(`The step must be a whole number of seconds, of at least ${MIN_ALIGNMENT_PERIOD} seconds.`);
48
+ }
49
+ const requestedCount = Math.floor(duration / requestedStep);
50
+ if (requestedCount < 1 || requestedCount > MAX_POINTS) {
51
+ throw new Error(`The series must contain between 1 and ${MAX_POINTS} points, but contains ${Math.max(requestedCount, 0)}.`);
52
+ }
53
+ // Metrics sampled less often than the step are read with a larger step, and at least one point, as for the
54
+ // point-in-time value.
55
+ const step = Math.max(requestedStep, metric.minPeriod ?? 0);
56
+ const count = step === requestedStep
57
+ ? requestedCount
58
+ : Math.max(Math.floor(duration / step), 1);
59
+ if (!this.graph.environment) {
60
+ throw new Error('The graph must be enriched with environment data to fetch a series.');
61
+ }
62
+ const environment = await GraphEnvironmentContext.forEnrichedGraph(this._context, this.graph);
63
+ const layouts = new BucketLayoutRegistry();
64
+ // Warnings, e.g. about time series relating to resources missing from the graph, don't affect the values.
65
+ const { values, groupBy, errors } = await queryMonitoringMetric(environment, metric, { end, period: step, count }, layouts, { nodes: [this.node] });
66
+ if (errors.length > 0) {
67
+ throw new Error(errors.map((e) => e.message).join(' '));
68
+ }
69
+ const points = values.get(this.node) ?? new Array(count).fill(undefined);
70
+ const start = new Date(end.getTime() - count * step * 1000);
71
+ return {
72
+ start,
73
+ step,
74
+ values: points.map((p) => p?.value ?? null),
75
+ ...(groupBy
76
+ ? {
77
+ groupBy,
78
+ groups: seriesGroups(points, monitoringMetricKind(metric) === GraphMetricKind.Rate ? 0 : null),
79
+ }
80
+ : {}),
81
+ bucketLayouts: layouts.all,
82
+ };
83
+ }
84
+ /**
85
+ * Returns the series of each group, from the points of the series.
86
+ *
87
+ * @param points The points of the series.
88
+ * @param missing The value of a group in a period where the point exists but not the group, e.g. `0` for rates.
89
+ * @returns The series of each group, sorted by key.
90
+ */
91
+ function seriesGroups(points, missing) {
92
+ const groups = new Map();
93
+ points.forEach((point, index) => {
94
+ for (const { key, value } of point?.groups ?? []) {
95
+ const serialized = JSON.stringify(key);
96
+ const group = groups.get(serialized) ?? {
97
+ key,
98
+ values: points.map((p) => (p ? missing : null)),
99
+ };
100
+ groups.set(serialized, group);
101
+ group.values[index] = value;
102
+ }
103
+ });
104
+ return [...groups.entries()]
105
+ .sort(([a], [b]) => a.localeCompare(b))
106
+ .map(([, group]) => group);
107
+ }
@@ -0,0 +1,23 @@
1
+ import { GraphFetchEnvironmentMetricSeries, type GraphMetricSeries } from '@causa/workspace-core';
2
+ import { type GraphNodeEntry } from '@causa/workspace-core/graph';
3
+ import { type MonitoringMetric } from '../../graph/environment/monitoring-catalog.js';
4
+ /**
5
+ * Implements {@link GraphFetchEnvironmentMetricSeries} for the metrics read from Cloud Monitoring.
6
+ */
7
+ export declare class GraphFetchEnvironmentMetricSeriesForGoogle extends GraphFetchEnvironmentMetricSeries {
8
+ _call(): Promise<GraphMetricSeries>;
9
+ /**
10
+ * Returns the requested node, from the graph.
11
+ *
12
+ * @returns The node, or `undefined` if it does not exist.
13
+ */
14
+ findNode(): GraphNodeEntry | undefined;
15
+ /**
16
+ * Returns the Cloud Monitoring metric providing the requested metric of the node.
17
+ *
18
+ * @returns The metric, or `undefined` if the node does not exist, has no resource, or the metric is not read from
19
+ * Cloud Monitoring for it.
20
+ */
21
+ findMetric(): MonitoringMetric | undefined;
22
+ _supports(): boolean;
23
+ }
@@ -0,0 +1,40 @@
1
+ import { callDeferred } from '@causa/workspace';
2
+ import { GraphFetchEnvironmentMetricSeries, } from '@causa/workspace-core';
3
+ import { listGraphNodes, } from '@causa/workspace-core/graph';
4
+ import { MONITORING_METRICS, } from '../../graph/environment/monitoring-catalog.js';
5
+ /**
6
+ * Implements {@link GraphFetchEnvironmentMetricSeries} for the metrics read from Cloud Monitoring.
7
+ */
8
+ export class GraphFetchEnvironmentMetricSeriesForGoogle extends GraphFetchEnvironmentMetricSeries {
9
+ async _call() {
10
+ return await callDeferred(this, import.meta.url);
11
+ }
12
+ /**
13
+ * Returns the requested node, from the graph.
14
+ *
15
+ * @returns The node, or `undefined` if it does not exist.
16
+ */
17
+ findNode() {
18
+ return listGraphNodes(this.graph).find((n) => n.id === this.node);
19
+ }
20
+ /**
21
+ * Returns the Cloud Monitoring metric providing the requested metric of the node.
22
+ *
23
+ * @returns The metric, or `undefined` if the node does not exist, has no resource, or the metric is not read from
24
+ * Cloud Monitoring for it.
25
+ */
26
+ findMetric() {
27
+ const node = this.findNode();
28
+ const resource = node?.node.data?.resource;
29
+ if (!node || typeof resource?.id !== 'string') {
30
+ return undefined;
31
+ }
32
+ return MONITORING_METRICS.find((m) => m.metric === this.metric &&
33
+ m.mapping.nodeType === node.type &&
34
+ m.mapping.resourceType === resource.type &&
35
+ (!m.measures || m.measures(node, this.graph)));
36
+ }
37
+ _supports() {
38
+ return !!this.findMetric();
39
+ }
40
+ }
@@ -0,0 +1,11 @@
1
+ import { GraphGetEnvironmentProvider, type GraphEnvironmentProvider } from '@causa/workspace-core';
2
+ /**
3
+ * Implements {@link GraphGetEnvironmentProvider} for Google Cloud.
4
+ * The fetchers read metrics and alerts from Cloud Monitoring, open error groups from Error Reporting as alerts, the
5
+ * state of Cloud Run services and Cloud Scheduler jobs, and build links to the Google Cloud console. Cloud Tasks
6
+ * queues, only known by a prefix of their name, are resolved by listing them.
7
+ */
8
+ export declare class GraphGetEnvironmentProviderForGoogle extends GraphGetEnvironmentProvider {
9
+ _call(): GraphEnvironmentProvider;
10
+ _supports(): boolean;
11
+ }
@@ -0,0 +1,20 @@
1
+ import { GraphGetEnvironmentProvider, } from '@causa/workspace-core';
2
+ import { CLOUD_TASKS_QUEUE_RESOLVER, GOOGLE_GRAPH_ENVIRONMENT_FETCHERS, GOOGLE_GRAPH_METRIC_DEFINITIONS, } from '../../graph/index.js';
3
+ /**
4
+ * Implements {@link GraphGetEnvironmentProvider} for Google Cloud.
5
+ * The fetchers read metrics and alerts from Cloud Monitoring, open error groups from Error Reporting as alerts, the
6
+ * state of Cloud Run services and Cloud Scheduler jobs, and build links to the Google Cloud console. Cloud Tasks
7
+ * queues, only known by a prefix of their name, are resolved by listing them.
8
+ */
9
+ export class GraphGetEnvironmentProviderForGoogle extends GraphGetEnvironmentProvider {
10
+ _call() {
11
+ return {
12
+ metrics: GOOGLE_GRAPH_METRIC_DEFINITIONS,
13
+ prefixResolvers: [CLOUD_TASKS_QUEUE_RESOLVER],
14
+ fetchers: GOOGLE_GRAPH_ENVIRONMENT_FETCHERS,
15
+ };
16
+ }
17
+ _supports() {
18
+ return true;
19
+ }
20
+ }
@@ -1 +1,3 @@
1
+ export { GraphFetchEnvironmentMetricSeriesForGoogle } from './fetch-environment-metric-series.js';
2
+ export { GraphGetEnvironmentProviderForGoogle } from './get-environment-provider.js';
1
3
  export { GraphListRulesForGoogle } from './list-rules.js';
@@ -1 +1,3 @@
1
+ export { GraphFetchEnvironmentMetricSeriesForGoogle } from './fetch-environment-metric-series.js';
2
+ export { GraphGetEnvironmentProviderForGoogle } from './get-environment-provider.js';
1
3
  export { GraphListRulesForGoogle } from './list-rules.js';
@@ -8,11 +8,11 @@ import { GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenera
8
8
  import { GooglePubSubWriteTopics } from './google-pubsub/index.js';
9
9
  import { GoogleServicesEnable } from './google-services/index.js';
10
10
  import { DatabaseQueryRecordsForSpanner, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, ModelGenerateTypeScriptDecoratorsForGoogleSpanner, ModelSchemaExtractDatabaseForGoogleSpanner, } from './google-spanner/index.js';
11
- import { GraphListRulesForGoogle } from './graph/index.js';
11
+ import { GraphFetchEnvironmentMetricSeriesForGoogle, GraphGetEnvironmentProviderForGoogle, GraphListRulesForGoogle, } from './graph/index.js';
12
12
  import { ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, } from './project/index.js';
13
13
  import { SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, } from './secret/index.js';
14
14
  import { ServiceContainerQueryLogsForCloudRun } from './service-container/index.js';
15
15
  import { ModelGenerateTypeScriptTriggerDecoratorsForGoogle } from './typescript/index.js';
16
16
  export function registerFunctions(context) {
17
- context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, DatabaseQueryRecordsForFirestore, DatabaseQueryRecordsForSpanner, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForCloudRunService, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicBrokerWaitForProcessingForPubSub, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, GraphListRulesForGoogle, ModelGenerateTypeScriptDecoratorsForGoogleFirestore, ModelGenerateTypeScriptDecoratorsForGoogleSpanner, ModelGenerateTypeScriptTriggerDecoratorsForGoogle, ModelSchemaExtractDatabaseForGoogleFirestore, ModelSchemaExtractDatabaseForGoogleSpanner, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, ServiceContainerQueryLogsForCloudRun);
17
+ context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, DatabaseQueryRecordsForFirestore, DatabaseQueryRecordsForSpanner, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForCloudRunService, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicBrokerWaitForProcessingForPubSub, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, GraphFetchEnvironmentMetricSeriesForGoogle, GraphGetEnvironmentProviderForGoogle, GraphListRulesForGoogle, ModelGenerateTypeScriptDecoratorsForGoogleFirestore, ModelGenerateTypeScriptDecoratorsForGoogleSpanner, ModelGenerateTypeScriptTriggerDecoratorsForGoogle, ModelSchemaExtractDatabaseForGoogleFirestore, ModelSchemaExtractDatabaseForGoogleSpanner, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, ServiceContainerQueryLogsForCloudRun);
18
18
  }
@@ -1,5 +1,5 @@
1
1
  import type { ServiceContainerConfiguration } from '@causa/workspace-core';
2
- import { GraphFact, type GraphContext, type GraphFactOutput, type GraphTemplatePart, type WorkspaceProject } from '@causa/workspace-core/graph';
2
+ import { GraphExtractionFact, type GraphContext, type GraphFactOutput, type GraphTemplatePart, type WorkspaceProject } from '@causa/workspace-core/graph';
3
3
  import { type TerraformModuleBlock } from '@causa/workspace-terraform';
4
4
  /**
5
5
  * A trigger defined in the `serviceContainer.triggers` configuration.
@@ -83,7 +83,7 @@ export type CloudRunService = {
83
83
  * The blocks of the Cloud Run module, evaluated against the configuration of the projects they deploy.
84
84
  * Blocks whose `configuration_file` does not name a project of the workspace cannot be mirrored, and are reported.
85
85
  */
86
- export declare class CloudRunFact extends GraphFact<CloudRunService[]> {
86
+ export declare class CloudRunFact extends GraphExtractionFact<CloudRunService[]> {
87
87
  compute(graph: GraphContext): Promise<GraphFactOutput<CloudRunService[]>>;
88
88
  /**
89
89
  * Evaluates a Cloud Run module block, mirroring the defaults of the module.
@@ -1,4 +1,4 @@
1
- import { GraphFact, ProjectsFact, serviceId, } from '@causa/workspace-core/graph';
1
+ import { GraphExtractionFact, ProjectsFact, serviceId, } from '@causa/workspace-core/graph';
2
2
  import { modulesWithSource, TerraformProjectsFact, } from '@causa/workspace-terraform';
3
3
  import { argumentOrConfiguration, projectOfConfigurationFile, } from './terraform-modules.js';
4
4
  /**
@@ -13,7 +13,7 @@ const MIRRORED_CLOUD_RUN_MAJOR = 1;
13
13
  * The blocks of the Cloud Run module, evaluated against the configuration of the projects they deploy.
14
14
  * Blocks whose `configuration_file` does not name a project of the workspace cannot be mirrored, and are reported.
15
15
  */
16
- export class CloudRunFact extends GraphFact {
16
+ export class CloudRunFact extends GraphExtractionFact {
17
17
  async compute(graph) {
18
18
  const [terraformProjects, projects] = await Promise.all([
19
19
  graph.get(TerraformProjectsFact),
@@ -0,0 +1,6 @@
1
+ import { type GraphEnvironmentFetcher } from '@causa/workspace-core';
2
+ /**
3
+ * Reads the alerts open at the end of the evaluation window from Cloud Monitoring, and attaches them to the nodes of
4
+ * their resources.
5
+ */
6
+ export declare const googleCloudMonitoringAlerts: GraphEnvironmentFetcher;
@@ -0,0 +1,106 @@
1
+ import { GraphAlertSeverity, } from '@causa/workspace-core';
2
+ import { alertLabelValue, listOpenAlerts, } from '../monitoring-alerts.js';
3
+ import { MONITORING_RESOURCE_MAPPINGS } from '../monitoring-catalog.js';
4
+ import { findMonitoredNode, prepareMonitoringMapping, readMonitoringResourceKey, } from '../monitoring-mappings.js';
5
+ import { googleResources } from '../resources.js';
6
+ import { consoleUrl } from './console-links.js';
7
+ /**
8
+ * The severities of alerts, keyed by the severity of their policy.
9
+ */
10
+ const SEVERITIES = {
11
+ CRITICAL: GraphAlertSeverity.Critical,
12
+ ERROR: GraphAlertSeverity.Error,
13
+ WARNING: GraphAlertSeverity.Warning,
14
+ };
15
+ /**
16
+ * Converts an alert returned by the Alerts API to a {@link GraphAlert}.
17
+ *
18
+ * @param alert The alert.
19
+ * @param project The GCP project from which the alert was listed.
20
+ * @returns The alert.
21
+ */
22
+ function toGraphAlert(alert, project) {
23
+ const severity = SEVERITIES[alert.policy?.severity ?? ''];
24
+ const id = alert.name?.split('/').at(-1);
25
+ return {
26
+ title: alert.policy?.displayName,
27
+ ...(severity ? { severity } : {}),
28
+ openedAt: new Date(alert.openTime),
29
+ ...(id
30
+ ? { url: consoleUrl(['monitoring', 'alerting', 'alerts', id], project) }
31
+ : {}),
32
+ };
33
+ }
34
+ /**
35
+ * Describes the resource of an alert in a warning.
36
+ *
37
+ * @param resource The resource of the alert.
38
+ * @returns The description.
39
+ */
40
+ function describeResource(resource) {
41
+ if (!resource) {
42
+ return 'a resource of unknown type';
43
+ }
44
+ const labels = Object.entries(resource.labels)
45
+ .filter(([name]) => name !== 'value' && name !== 'monitored_resource')
46
+ .sort(([a], [b]) => a.localeCompare(b))
47
+ .map(([name, value]) => `${name}=${value}`);
48
+ return `the '${resource.type}' resource (${labels.join(', ')})`;
49
+ }
50
+ /**
51
+ * Reads the alerts open at the end of the evaluation window from Cloud Monitoring, and attaches them to the nodes of
52
+ * their resources.
53
+ */
54
+ export const googleCloudMonitoringAlerts = {
55
+ name: 'googleCloudMonitoringAlerts',
56
+ description: "The `alerts` open at the end of the evaluation window, read from Cloud Monitoring in the projects of the graph's resources, and attached to the nodes of their resources. Alerts that cannot be attached to a node are added to the environment.",
57
+ fetch: async (environment) => {
58
+ const projects = [
59
+ ...new Set(googleResources(environment).map(({ segments }) => segments.projects)),
60
+ ]
61
+ .filter((p) => !!p)
62
+ .sort();
63
+ const warnings = [];
64
+ const listed = await Promise.all(projects.map(async (project) => {
65
+ try {
66
+ return await listOpenAlerts(environment.context, project, environment.at);
67
+ }
68
+ catch (error) {
69
+ warnings.push({
70
+ message: `Failed to list the alerts in project '${project}': ${error.message ?? error}`,
71
+ });
72
+ return [];
73
+ }
74
+ }));
75
+ const alerts = listed.flat();
76
+ if (alerts.length === 0) {
77
+ return { warnings };
78
+ }
79
+ // Only the mappings that may apply to an alert are prepared, as preparing a mapping may require listing resources,
80
+ // e.g. the backends of load balancers.
81
+ const candidates = (await Promise.all(MONITORING_RESOURCE_MAPPINGS.filter((mapping) => alerts.some(({ resource }) => resource?.type === mapping.monitoredResource &&
82
+ readMonitoringResourceKey(mapping, resource.project, (label) => alertLabelValue(resource, label)))).map((mapping) => prepareMonitoringMapping(environment, mapping)))).filter((m) => !!m);
83
+ const nodes = {};
84
+ const unattached = [];
85
+ for (const { alert, project, resource } of alerts) {
86
+ const graphAlert = toGraphAlert(alert, project);
87
+ const { node } = resource
88
+ ? findMonitoredNode(environment, candidates.filter(({ mapping }) => mapping.monitoredResource === resource.type), resource.project, (label) => alertLabelValue(resource, label))
89
+ : {};
90
+ if (node) {
91
+ nodes[node.id] ??= { alerts: [] };
92
+ nodes[node.id].alerts.push(graphAlert);
93
+ continue;
94
+ }
95
+ unattached.push(graphAlert);
96
+ warnings.push({
97
+ message: `The alert of policy '${graphAlert.title}' on ${describeResource(resource)} could not be attached to a node, and is added to the environment.`,
98
+ });
99
+ }
100
+ return {
101
+ nodes,
102
+ ...(unattached.length > 0 ? { alerts: unattached } : {}),
103
+ warnings,
104
+ };
105
+ },
106
+ };
@@ -0,0 +1,5 @@
1
+ import type { GraphEnvironmentFetcher } from '@causa/workspace-core';
2
+ /**
3
+ * Reads the metrics of nodes from Cloud Monitoring, over the evaluation window.
4
+ */
5
+ export declare const googleCloudMonitoring: GraphEnvironmentFetcher;
@@ -0,0 +1,13 @@
1
+ import { MONITORING_METRICS } from '../monitoring-catalog.js';
2
+ import { queryPointInTimeMetrics } from '../monitoring-query.js';
3
+ /**
4
+ * Reads the metrics of nodes from Cloud Monitoring, over the evaluation window.
5
+ */
6
+ export const googleCloudMonitoring = {
7
+ name: 'googleCloudMonitoring',
8
+ description: 'The metrics of Cloud Run services, API routers, Pub/Sub topics and subscriptions, Cloud Tasks queues, and Spanner, Firestore, and BigQuery databases, read from Cloud Monitoring.',
9
+ fetch: async (environment) => {
10
+ const { nodes, bucketLayouts, warnings } = await queryPointInTimeMetrics(environment, MONITORING_METRICS);
11
+ return { nodes, bucketLayouts, warnings };
12
+ },
13
+ };
@@ -0,0 +1,5 @@
1
+ import type { GraphEnvironmentFetcher } from '@causa/workspace-core';
2
+ /**
3
+ * Reads the deployments of Cloud Run services.
4
+ */
5
+ export declare const googleCloudRun: GraphEnvironmentFetcher;