@causa/workspace-google 1.3.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 (98) hide show
  1. package/README.md +23 -0
  2. package/dist/functions/event-topic/create-backfill-source-bigquery.call.js +2 -2
  3. package/dist/functions/event-topic/query-events-bigquery.call.js +2 -2
  4. package/dist/functions/google-app-check/generate-token.js +3 -1
  5. package/dist/functions/google-firebase-storage/merge-rules.js +3 -1
  6. package/dist/functions/google-firestore/generate-typescript-decorators.js +2 -1
  7. package/dist/functions/google-firestore/merge-rules.js +3 -1
  8. package/dist/functions/google-identity-platform/generate-custom-token.js +7 -2
  9. package/dist/functions/google-pubsub/index.d.ts +1 -1
  10. package/dist/functions/google-pubsub/index.js +1 -1
  11. package/dist/functions/google-pubsub/write-topics.d.ts +7 -0
  12. package/dist/functions/google-pubsub/write-topics.js +17 -3
  13. package/dist/functions/google-services/enable.js +3 -1
  14. package/dist/functions/google-spanner/generate-typescript-decorators.js +2 -1
  15. package/dist/functions/google-spanner/list-databases.d.ts +1 -1
  16. package/dist/functions/google-spanner/write-databases.js +7 -2
  17. package/dist/functions/graph/fetch-environment-metric-series.call.d.ts +3 -0
  18. package/dist/functions/graph/fetch-environment-metric-series.call.js +107 -0
  19. package/dist/functions/graph/fetch-environment-metric-series.d.ts +23 -0
  20. package/dist/functions/graph/fetch-environment-metric-series.js +40 -0
  21. package/dist/functions/graph/get-environment-provider.d.ts +11 -0
  22. package/dist/functions/graph/get-environment-provider.js +20 -0
  23. package/dist/functions/graph/index.d.ts +3 -0
  24. package/dist/functions/graph/index.js +3 -0
  25. package/dist/functions/graph/list-rules.d.ts +11 -0
  26. package/dist/functions/graph/list-rules.js +16 -0
  27. package/dist/functions/index.js +2 -1
  28. package/dist/graph/cloud-run.d.ts +98 -0
  29. package/dist/graph/cloud-run.js +102 -0
  30. package/dist/graph/environment/fetchers/cloud-monitoring-alerts.d.ts +6 -0
  31. package/dist/graph/environment/fetchers/cloud-monitoring-alerts.js +106 -0
  32. package/dist/graph/environment/fetchers/cloud-monitoring.d.ts +5 -0
  33. package/dist/graph/environment/fetchers/cloud-monitoring.js +13 -0
  34. package/dist/graph/environment/fetchers/cloud-run.d.ts +5 -0
  35. package/dist/graph/environment/fetchers/cloud-run.js +88 -0
  36. package/dist/graph/environment/fetchers/cloud-scheduler.d.ts +5 -0
  37. package/dist/graph/environment/fetchers/cloud-scheduler.js +95 -0
  38. package/dist/graph/environment/fetchers/console-links.d.ts +14 -0
  39. package/dist/graph/environment/fetchers/console-links.js +132 -0
  40. package/dist/graph/environment/fetchers/error-reporting.d.ts +5 -0
  41. package/dist/graph/environment/fetchers/error-reporting.js +98 -0
  42. package/dist/graph/environment/fetchers/index.d.ts +5 -0
  43. package/dist/graph/environment/fetchers/index.js +17 -0
  44. package/dist/graph/environment/index.d.ts +9 -0
  45. package/dist/graph/environment/index.js +9 -0
  46. package/dist/graph/environment/load-balancer-backends.d.ts +82 -0
  47. package/dist/graph/environment/load-balancer-backends.js +126 -0
  48. package/dist/graph/environment/monitoring-alerts.d.ts +60 -0
  49. package/dist/graph/environment/monitoring-alerts.js +67 -0
  50. package/dist/graph/environment/monitoring-catalog.d.ts +95 -0
  51. package/dist/graph/environment/monitoring-catalog.js +356 -0
  52. package/dist/graph/environment/monitoring-mappings.d.ts +133 -0
  53. package/dist/graph/environment/monitoring-mappings.js +97 -0
  54. package/dist/graph/environment/monitoring-query.d.ts +116 -0
  55. package/dist/graph/environment/monitoring-query.js +314 -0
  56. package/dist/graph/environment/monitoring-values.d.ts +41 -0
  57. package/dist/graph/environment/monitoring-values.js +119 -0
  58. package/dist/graph/environment/resource-resolvers.d.ts +7 -0
  59. package/dist/graph/environment/resource-resolvers.js +57 -0
  60. package/dist/graph/environment/resources.d.ts +45 -0
  61. package/dist/graph/environment/resources.js +49 -0
  62. package/dist/graph/ids.d.ts +35 -0
  63. package/dist/graph/ids.js +44 -0
  64. package/dist/graph/index.d.ts +7 -0
  65. package/dist/graph/index.js +7 -0
  66. package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.d.ts +6 -0
  67. package/dist/graph/rules/configuration/enqueues-assumed-from-google-task-trigger.js +39 -0
  68. package/dist/graph/rules/configuration/firestore.d.ts +11 -0
  69. package/dist/graph/rules/configuration/firestore.js +93 -0
  70. package/dist/graph/rules/configuration/index.d.ts +5 -0
  71. package/dist/graph/rules/configuration/index.js +11 -0
  72. package/dist/graph/rules/configuration/service-container.d.ts +13 -0
  73. package/dist/graph/rules/configuration/service-container.js +123 -0
  74. package/dist/graph/rules/terraform/api-router.d.ts +15 -0
  75. package/dist/graph/rules/terraform/api-router.js +117 -0
  76. package/dist/graph/rules/terraform/cloud-run-cron-triggers.d.ts +11 -0
  77. package/dist/graph/rules/terraform/cloud-run-cron-triggers.js +67 -0
  78. package/dist/graph/rules/terraform/cloud-run-grants.d.ts +16 -0
  79. package/dist/graph/rules/terraform/cloud-run-grants.js +118 -0
  80. package/dist/graph/rules/terraform/cloud-run-module.d.ts +6 -0
  81. package/dist/graph/rules/terraform/cloud-run-module.js +29 -0
  82. package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.d.ts +14 -0
  83. package/dist/graph/rules/terraform/cloud-run-pubsub-triggers.js +103 -0
  84. package/dist/graph/rules/terraform/cloud-run-services.d.ts +12 -0
  85. package/dist/graph/rules/terraform/cloud-run-services.js +61 -0
  86. package/dist/graph/rules/terraform/cloud-run-tasks-triggers.d.ts +14 -0
  87. package/dist/graph/rules/terraform/cloud-run-tasks-triggers.js +97 -0
  88. package/dist/graph/rules/terraform/event-topics.d.ts +18 -0
  89. package/dist/graph/rules/terraform/event-topics.js +193 -0
  90. package/dist/graph/rules/terraform/index.d.ts +5 -0
  91. package/dist/graph/rules/terraform/index.js +13 -0
  92. package/dist/graph/rules/terraform/spanner-databases.d.ts +17 -0
  93. package/dist/graph/rules/terraform/spanner-databases.js +180 -0
  94. package/dist/graph/spanner.d.ts +106 -0
  95. package/dist/graph/spanner.js +118 -0
  96. package/dist/graph/terraform-modules.d.ts +87 -0
  97. package/dist/graph/terraform-modules.js +107 -0
  98. package/package.json +30 -29
@@ -0,0 +1,132 @@
1
+ import { googleResources } from '../resources.js';
2
+ /**
3
+ * The base URL of the Google Cloud console.
4
+ */
5
+ const CONSOLE_URL = 'https://console.cloud.google.com';
6
+ /**
7
+ * Builds the URL of a page of the Google Cloud console.
8
+ *
9
+ * @param segments The segments of the path of the page, which are encoded.
10
+ * @param project The GCP project.
11
+ * @param query Additional query parameters.
12
+ * @returns The URL.
13
+ */
14
+ export function consoleUrl(segments, project, query = {}) {
15
+ const path = segments.map(encodeURIComponent).join('/');
16
+ const parameters = new URLSearchParams({ project, ...query });
17
+ return `${CONSOLE_URL}/${path}?${parameters}`;
18
+ }
19
+ /**
20
+ * The builders of links, keyed by resource type.
21
+ */
22
+ const LINK_BUILDERS = {
23
+ 'run.googleapis.com/Service': ({ projects, locations, services }) => [
24
+ {
25
+ label: 'Cloud Run service',
26
+ url: consoleUrl(['run', 'detail', locations, services, 'metrics'], projects),
27
+ },
28
+ {
29
+ label: 'Cloud Run service logs',
30
+ url: consoleUrl(['run', 'detail', locations, services, 'logs'], projects),
31
+ },
32
+ ],
33
+ 'pubsub.googleapis.com/Topic': ({ projects, topics }) => [
34
+ {
35
+ label: 'Pub/Sub topic',
36
+ url: consoleUrl(['cloudpubsub', 'topic', 'detail', topics], projects),
37
+ },
38
+ ],
39
+ 'pubsub.googleapis.com/Subscription': ({ projects, subscriptions }) => [
40
+ {
41
+ label: 'Pub/Sub subscription',
42
+ url: consoleUrl(['cloudpubsub', 'subscription', 'detail', subscriptions], projects),
43
+ },
44
+ ],
45
+ 'cloudtasks.googleapis.com/Queue': ({ projects, locations, queues }) => [
46
+ {
47
+ label: 'Cloud Tasks queue',
48
+ url: consoleUrl(['cloudtasks', 'queue', locations, queues, 'tasks'], projects),
49
+ },
50
+ ],
51
+ 'cloudscheduler.googleapis.com/Job': ({ projects }) => [
52
+ {
53
+ label: 'Cloud Scheduler jobs',
54
+ url: consoleUrl(['cloudscheduler'], projects),
55
+ },
56
+ ],
57
+ 'spanner.googleapis.com/Instance': ({ projects, instances }) => [
58
+ {
59
+ label: 'Spanner instance',
60
+ url: consoleUrl(['spanner', 'instances', instances, 'details', 'databases'], projects),
61
+ },
62
+ ],
63
+ 'spanner.googleapis.com/Database': ({ projects, instances, databases }) => [
64
+ {
65
+ label: 'Spanner database',
66
+ url: consoleUrl([
67
+ 'spanner',
68
+ 'instances',
69
+ instances,
70
+ 'databases',
71
+ databases,
72
+ 'details',
73
+ 'tables',
74
+ ], projects),
75
+ },
76
+ ],
77
+ 'firestore.googleapis.com/Database': ({ projects, databases }) => [
78
+ {
79
+ label: 'Firestore database',
80
+ url: consoleUrl([
81
+ 'firestore',
82
+ 'databases',
83
+ databases === '(default)' ? '-default-' : databases,
84
+ 'data',
85
+ ], projects),
86
+ },
87
+ ],
88
+ 'bigquery.googleapis.com/Dataset': ({ projects, datasets }) => [
89
+ {
90
+ label: 'BigQuery dataset',
91
+ url: consoleUrl(['bigquery'], projects, {
92
+ p: projects,
93
+ d: datasets,
94
+ page: 'dataset',
95
+ }),
96
+ },
97
+ ],
98
+ 'bigquery.googleapis.com/Table': ({ projects, datasets, tables }) => [
99
+ {
100
+ label: 'BigQuery table',
101
+ url: consoleUrl(['bigquery'], projects, {
102
+ p: projects,
103
+ d: datasets,
104
+ t: tables,
105
+ page: 'table',
106
+ }),
107
+ },
108
+ ],
109
+ 'compute.googleapis.com/UrlMap': ({ projects }) => [
110
+ {
111
+ label: 'Load balancers',
112
+ url: consoleUrl(['net-services', 'loadbalancing', 'list', 'loadBalancers'], projects),
113
+ },
114
+ ],
115
+ };
116
+ /**
117
+ * Builds links to the Google Cloud console for the nodes' resources.
118
+ */
119
+ export const googleConsoleLinks = {
120
+ name: 'googleConsoleLinks',
121
+ description: 'The `links` of nodes whose resource is a Google Cloud resource, to the Google Cloud console.',
122
+ fetch: async (environment) => {
123
+ const nodes = {};
124
+ for (const { node, type, segments } of googleResources(environment)) {
125
+ const builder = LINK_BUILDERS[type];
126
+ if (builder && segments.projects) {
127
+ nodes[node.id] = { data: { links: builder(segments) } };
128
+ }
129
+ }
130
+ return { nodes };
131
+ },
132
+ };
@@ -0,0 +1,5 @@
1
+ import { type GraphEnvironmentFetcher } from '@causa/workspace-core';
2
+ /**
3
+ * Reads the open groups of errors of Cloud Run services from Error Reporting, as alerts.
4
+ */
5
+ export declare const googleErrorReporting: GraphEnvironmentFetcher;
@@ -0,0 +1,98 @@
1
+ import { GraphAlertSeverity, } from '@causa/workspace-core';
2
+ import { googleResources } from '../resources.js';
3
+ import { consoleUrl } from './console-links.js';
4
+ /**
5
+ * The longest period over which Error Reporting can list groups. Periods always end now.
6
+ */
7
+ const PERIOD = 'PERIOD_30_DAYS';
8
+ /**
9
+ * The length of {@link PERIOD}, in milliseconds.
10
+ */
11
+ const PERIOD_LENGTH = 30 * 24 * 3600 * 1000;
12
+ /**
13
+ * Converts the statistics of an open error group to an alert, titled with the first line of the message of the group's
14
+ * representative error.
15
+ *
16
+ * @param stats The statistics of the group.
17
+ * @param project The GCP project of the group.
18
+ * @returns The alert, or `undefined` if the group has never been seen.
19
+ */
20
+ function toGraphAlert(stats, project) {
21
+ if (!stats.firstSeenTime) {
22
+ return undefined;
23
+ }
24
+ const groupId = stats.group?.groupId ?? undefined;
25
+ const message = stats.representative?.message?.split('\n', 1)[0].trim();
26
+ return {
27
+ title: message || `Error group ${groupId}`,
28
+ severity: GraphAlertSeverity.Error,
29
+ openedAt: new Date(stats.firstSeenTime),
30
+ ...(groupId
31
+ ? { url: consoleUrl(['errors', 'detail', groupId], project) }
32
+ : {}),
33
+ };
34
+ }
35
+ /**
36
+ * Reads the open groups of errors of Cloud Run services from Error Reporting, as alerts.
37
+ */
38
+ export const googleErrorReporting = {
39
+ name: 'googleErrorReporting',
40
+ description: 'The `alerts` of Cloud Run services for their open Error Reporting groups, i.e. groups with occurrences during the last 30 days, first seen before the end of the evaluation window. Acknowledged, resolved, and muted groups are excluded.',
41
+ fetch: async (environment) => {
42
+ const services = googleResources(environment, 'run.googleapis.com/Service', 'service');
43
+ if (services.length === 0) {
44
+ return {};
45
+ }
46
+ const { at } = environment;
47
+ if (Date.now() - at.getTime() > PERIOD_LENGTH) {
48
+ return {
49
+ warnings: [
50
+ {
51
+ message: 'Error groups cannot be listed for an evaluation window ending more than 30 days ago.',
52
+ },
53
+ ],
54
+ };
55
+ }
56
+ const { GoogleApisService } = await import('../../../services/google-apis.js');
57
+ const client = await environment.context
58
+ .service(GoogleApisService)
59
+ .getClient('clouderrorreporting', 'v1beta1', {});
60
+ const nodes = {};
61
+ const warnings = [];
62
+ await Promise.all(services.map(async ({ node, segments }) => {
63
+ const stats = [];
64
+ try {
65
+ let pageToken;
66
+ do {
67
+ const { data } = await client.projects.groupStats.list({
68
+ projectName: `projects/${segments.projects}`,
69
+ 'serviceFilter.service': segments.services,
70
+ 'timeRange.period': PERIOD,
71
+ pageSize: 100,
72
+ pageToken,
73
+ });
74
+ stats.push(...(data.errorGroupStats ?? []));
75
+ pageToken = data.nextPageToken ?? undefined;
76
+ } while (pageToken);
77
+ }
78
+ catch (error) {
79
+ warnings.push({
80
+ message: `Failed to list the error groups of '${node.id}': ${error.message ?? error}`,
81
+ });
82
+ return;
83
+ }
84
+ const alerts = stats.flatMap((groupStats) => {
85
+ // An unspecified resolution status is interpreted as open.
86
+ const status = groupStats.group?.resolutionStatus ?? 'OPEN';
87
+ const alert = status === 'OPEN'
88
+ ? toGraphAlert(groupStats, segments.projects)
89
+ : undefined;
90
+ return alert && alert.openedAt < at ? [alert] : [];
91
+ });
92
+ if (alerts.length > 0) {
93
+ nodes[node.id] = { alerts };
94
+ }
95
+ }));
96
+ return { nodes, warnings };
97
+ },
98
+ };
@@ -0,0 +1,5 @@
1
+ import type { GraphEnvironmentFetcher } from '@causa/workspace-core';
2
+ /**
3
+ * The fetchers of environment data and metrics for Google Cloud.
4
+ */
5
+ export declare const GOOGLE_GRAPH_ENVIRONMENT_FETCHERS: GraphEnvironmentFetcher[];
@@ -0,0 +1,17 @@
1
+ import { googleCloudMonitoringAlerts } from './cloud-monitoring-alerts.js';
2
+ import { googleCloudMonitoring } from './cloud-monitoring.js';
3
+ import { googleCloudRun } from './cloud-run.js';
4
+ import { googleCloudScheduler } from './cloud-scheduler.js';
5
+ import { googleConsoleLinks } from './console-links.js';
6
+ import { googleErrorReporting } from './error-reporting.js';
7
+ /**
8
+ * The fetchers of environment data and metrics for Google Cloud.
9
+ */
10
+ export const GOOGLE_GRAPH_ENVIRONMENT_FETCHERS = [
11
+ googleCloudMonitoring,
12
+ googleCloudMonitoringAlerts,
13
+ googleCloudRun,
14
+ googleCloudScheduler,
15
+ googleConsoleLinks,
16
+ googleErrorReporting,
17
+ ];
@@ -0,0 +1,9 @@
1
+ export * from './fetchers/index.js';
2
+ export * from './load-balancer-backends.js';
3
+ export * from './monitoring-alerts.js';
4
+ export * from './monitoring-catalog.js';
5
+ export * from './monitoring-mappings.js';
6
+ export * from './monitoring-query.js';
7
+ export * from './monitoring-values.js';
8
+ export * from './resource-resolvers.js';
9
+ export * from './resources.js';
@@ -0,0 +1,9 @@
1
+ export * from './fetchers/index.js';
2
+ export * from './load-balancer-backends.js';
3
+ export * from './monitoring-alerts.js';
4
+ export * from './monitoring-catalog.js';
5
+ export * from './monitoring-mappings.js';
6
+ export * from './monitoring-query.js';
7
+ export * from './monitoring-values.js';
8
+ export * from './resource-resolvers.js';
9
+ export * from './resources.js';
@@ -0,0 +1,82 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import { GraphEnvironmentFact, type GraphEnvironmentContext, type GraphFactOutput } from '@causa/workspace-core/graph';
3
+ import { type MonitoringLabel, type MonitoringResourceMapping } from './monitoring-mappings.js';
4
+ import { type GoogleResource } from './resources.js';
5
+ /**
6
+ * Maps load balancer labels to the URL maps of the graph, i.e. API routers.
7
+ */
8
+ export declare const URL_MAP_MAPPING: MonitoringResourceMapping;
9
+ /**
10
+ * A backend of load balancers, which routes requests to a resource.
11
+ */
12
+ export type LoadBalancerBackend = {
13
+ /**
14
+ * The kind of the backend.
15
+ */
16
+ readonly kind: LoadBalancerBackendKind;
17
+ /**
18
+ * The GCP project of the backend.
19
+ */
20
+ readonly project: string;
21
+ /**
22
+ * The values of the key labels of the kind identifying the backend.
23
+ */
24
+ readonly labels: Readonly<Record<MonitoringLabel, string>>;
25
+ /**
26
+ * The full resource name of the resource the backend routes requests to, e.g. a Cloud Run service.
27
+ */
28
+ readonly resource: string;
29
+ };
30
+ /**
31
+ * A kind of load balancer backend, routing requests to the resources of nodes of a type.
32
+ */
33
+ export type LoadBalancerBackendKind = {
34
+ /**
35
+ * The type of the nodes requests are routed to.
36
+ */
37
+ readonly nodeType: string;
38
+ /**
39
+ * The type of the nodes' resource, in their `data.resource.type`.
40
+ */
41
+ readonly resourceType: string;
42
+ /**
43
+ * The labels of load balancer time series and alerts identifying a backend of this kind within a project.
44
+ */
45
+ readonly keyLabels: readonly MonitoringLabel[];
46
+ /**
47
+ * Lists the backends of this kind. It throws if some backends cannot be listed.
48
+ *
49
+ * @param context The context, from which the `GoogleApisService` is obtained.
50
+ * @param projects The projects of the graph's URL maps, in which backends are listed.
51
+ * @param resources The resources of the kind's types, e.g. to only list regional backends in their regions.
52
+ * @returns The backends.
53
+ */
54
+ list(context: WorkspaceContext, projects: readonly string[], resources: readonly GoogleResource[]): Promise<LoadBalancerBackend[]>;
55
+ };
56
+ /**
57
+ * Serverless network endpoint groups, routing requests to Cloud Run services.
58
+ * They are listed in the regions of the services, in the projects of the URL maps.
59
+ */
60
+ export declare const SERVERLESS_NEG_BACKENDS: LoadBalancerBackendKind;
61
+ /**
62
+ * The kinds of backends through which requests can be routed to nodes.
63
+ */
64
+ export declare const LOAD_BALANCER_BACKEND_KINDS: readonly LoadBalancerBackendKind[];
65
+ /**
66
+ * Lists the backends of all kinds, in the projects of the URL maps of the graph. No API is called if the graph has no
67
+ * URL map. The fact fails if some backends cannot be listed.
68
+ */
69
+ export declare class LoadBalancerBackendsFact extends GraphEnvironmentFact<readonly LoadBalancerBackend[]> {
70
+ compute(environment: GraphEnvironmentContext): Promise<GraphFactOutput<readonly LoadBalancerBackend[]>>;
71
+ }
72
+ /**
73
+ * Builds the mapping of the backends of a kind, from load balancer labels to the resources they route requests to.
74
+ *
75
+ * @param kind The kind of backends.
76
+ * @returns The mapping.
77
+ */
78
+ export declare function loadBalancerBackendMapping(kind: LoadBalancerBackendKind): MonitoringResourceMapping;
79
+ /**
80
+ * The mappings of load balancer labels to the resources backends route requests to, one per kind of backend.
81
+ */
82
+ export declare const LOAD_BALANCER_BACKEND_MAPPINGS: readonly MonitoringResourceMapping[];
@@ -0,0 +1,126 @@
1
+ import { GraphEnvironmentFact, } from '@causa/workspace-core/graph';
2
+ import { templateMapping, } from './monitoring-mappings.js';
3
+ import { googleResources } from './resources.js';
4
+ /**
5
+ * Maps load balancer labels to the URL maps of the graph, i.e. API routers.
6
+ */
7
+ export const URL_MAP_MAPPING = templateMapping({
8
+ nodeType: 'apiRouter',
9
+ resourceType: 'compute.googleapis.com/UrlMap',
10
+ monitoredResource: 'https_lb_rule',
11
+ name: 'projects/{project}/global/urlMaps/{resource.label.url_map_name}',
12
+ });
13
+ /**
14
+ * Serverless network endpoint groups, routing requests to Cloud Run services.
15
+ * They are listed in the regions of the services, in the projects of the URL maps.
16
+ */
17
+ export const SERVERLESS_NEG_BACKENDS = {
18
+ nodeType: 'service',
19
+ resourceType: 'run.googleapis.com/Service',
20
+ keyLabels: ['resource.label.backend_scope', 'resource.label.backend_name'],
21
+ list: async (context, projects, resources) => {
22
+ const locations = new Map();
23
+ for (const { segments } of resources) {
24
+ const { projects: project, locations: region } = segments;
25
+ if (project && region && projects.includes(project)) {
26
+ locations.set(`projects/${project}/regions/${region}`, {
27
+ project,
28
+ region,
29
+ });
30
+ }
31
+ }
32
+ if (locations.size === 0) {
33
+ return [];
34
+ }
35
+ const { GoogleApisService } = await import('../../services/google-apis.js');
36
+ const client = await context
37
+ .service(GoogleApisService)
38
+ .getClient('compute', 'v1', {});
39
+ const backends = await Promise.all([...locations.values()].map(async ({ project, region }) => {
40
+ const groups = [];
41
+ try {
42
+ let pageToken;
43
+ do {
44
+ const { data } = await client.regionNetworkEndpointGroups.list({
45
+ project,
46
+ region,
47
+ pageToken,
48
+ });
49
+ groups.push(...(data.items ?? []));
50
+ pageToken = data.nextPageToken ?? undefined;
51
+ } while (pageToken);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`Failed to list the network endpoint groups in project '${project}' and region '${region}': ${error.message ?? error}`, { cause: error });
55
+ }
56
+ return groups.flatMap((group) => {
57
+ const service = group.cloudRun?.service;
58
+ if (group.networkEndpointType !== 'SERVERLESS' ||
59
+ !group.name ||
60
+ !service) {
61
+ return [];
62
+ }
63
+ return [
64
+ {
65
+ kind: SERVERLESS_NEG_BACKENDS,
66
+ project,
67
+ labels: {
68
+ 'resource.label.backend_scope': region,
69
+ 'resource.label.backend_name': group.name,
70
+ },
71
+ resource: `projects/${project}/locations/${region}/services/${service}`,
72
+ },
73
+ ];
74
+ });
75
+ }));
76
+ return backends.flat();
77
+ },
78
+ };
79
+ /**
80
+ * The kinds of backends through which requests can be routed to nodes.
81
+ */
82
+ export const LOAD_BALANCER_BACKEND_KINDS = [
83
+ SERVERLESS_NEG_BACKENDS,
84
+ ];
85
+ /**
86
+ * Lists the backends of all kinds, in the projects of the URL maps of the graph. No API is called if the graph has no
87
+ * URL map. The fact fails if some backends cannot be listed.
88
+ */
89
+ export class LoadBalancerBackendsFact extends GraphEnvironmentFact {
90
+ async compute(environment) {
91
+ const projects = [
92
+ ...new Set(googleResources(environment, URL_MAP_MAPPING.resourceType, URL_MAP_MAPPING.nodeType).map(({ segments }) => segments.projects)),
93
+ ];
94
+ if (projects.length === 0) {
95
+ return { value: [] };
96
+ }
97
+ const backends = await Promise.all(LOAD_BALANCER_BACKEND_KINDS.map((kind) => kind.list(environment.context, projects, googleResources(environment, kind.resourceType, kind.nodeType))));
98
+ return { value: backends.flat() };
99
+ }
100
+ }
101
+ /**
102
+ * Builds the mapping of the backends of a kind, from load balancer labels to the resources they route requests to.
103
+ *
104
+ * @param kind The kind of backends.
105
+ * @returns The mapping.
106
+ */
107
+ export function loadBalancerBackendMapping(kind) {
108
+ return {
109
+ nodeType: kind.nodeType,
110
+ resourceType: kind.resourceType,
111
+ monitoredResource: URL_MAP_MAPPING.monitoredResource,
112
+ keyLabels: kind.keyLabels,
113
+ resolver: async (environment) => {
114
+ const backends = await environment.get(LoadBalancerBackendsFact);
115
+ const key = (project, labels) => JSON.stringify([project, ...kind.keyLabels.map((l) => labels[l])]);
116
+ const resources = new Map(backends
117
+ .filter((b) => b.kind === kind)
118
+ .map((b) => [key(b.project, b.labels), b.resource]));
119
+ return ({ project, labels }) => resources.get(key(project, labels));
120
+ },
121
+ };
122
+ }
123
+ /**
124
+ * The mappings of load balancer labels to the resources backends route requests to, one per kind of backend.
125
+ */
126
+ export const LOAD_BALANCER_BACKEND_MAPPINGS = LOAD_BALANCER_BACKEND_KINDS.map(loadBalancerBackendMapping);
@@ -0,0 +1,60 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import type { monitoring_v3 } from 'googleapis';
3
+ import { type MonitoringLabel } from './monitoring-mappings.js';
4
+ /**
5
+ * An alert, as returned by the Cloud Monitoring Alerts API.
6
+ */
7
+ export type MonitoringAlert = monitoring_v3.Schema$Alert;
8
+ /**
9
+ * The monitored resource of an alert.
10
+ */
11
+ export type MonitoringAlertResource = {
12
+ /**
13
+ * The monitored resource type, e.g. `cloud_run_revision`.
14
+ */
15
+ readonly type: string;
16
+ /**
17
+ * The GCP project of the resource: the `project_id` label, or the project from which the alert was listed.
18
+ */
19
+ readonly project: string;
20
+ /**
21
+ * The labels of the alert, keyed by name. Resource labels take precedence over metric labels with the same name, as
22
+ * alerts aggregating away the resource type only keep metric labels.
23
+ */
24
+ readonly labels: Readonly<Record<string, string>>;
25
+ };
26
+ /**
27
+ * An alert open at a given time, along with its monitored resource.
28
+ */
29
+ export type OpenMonitoringAlert = {
30
+ /**
31
+ * The alert, as returned by the Alerts API.
32
+ */
33
+ readonly alert: MonitoringAlert;
34
+ /**
35
+ * The GCP project from which the alert was listed.
36
+ */
37
+ readonly project: string;
38
+ /**
39
+ * The monitored resource of the alert. It is `undefined` when its type is unknown.
40
+ */
41
+ readonly resource?: MonitoringAlertResource;
42
+ };
43
+ /**
44
+ * Returns the value of a label of the resource of an alert, read by its name regardless of its source, as alerts
45
+ * aggregating away the resource type only keep metric labels.
46
+ *
47
+ * @param resource The resource of the alert.
48
+ * @param label The label.
49
+ * @returns The value of the label.
50
+ */
51
+ export declare function alertLabelValue(resource: MonitoringAlertResource, label: MonitoringLabel): string | undefined;
52
+ /**
53
+ * Lists the alerts of a project that were open at a given time, along with their monitored resources.
54
+ *
55
+ * @param context The context, from which the `GoogleApisService` is obtained.
56
+ * @param project The GCP project.
57
+ * @param at The time at which the alerts were open.
58
+ * @returns The alerts.
59
+ */
60
+ export declare function listOpenAlerts(context: WorkspaceContext, project: string, at: Date): Promise<OpenMonitoringAlert[]>;
@@ -0,0 +1,67 @@
1
+ import { monitoringLabelName, } from './monitoring-mappings.js';
2
+ /**
3
+ * The resource type of alerts whose condition aggregates away the monitored resource type, e.g. a PromQL query
4
+ * aggregating a metric written against several resource types.
5
+ */
6
+ const PROMETHEUS_TARGET = 'prometheus_target';
7
+ /**
8
+ * Returns the value of a label of the resource of an alert, read by its name regardless of its source, as alerts
9
+ * aggregating away the resource type only keep metric labels.
10
+ *
11
+ * @param resource The resource of the alert.
12
+ * @param label The label.
13
+ * @returns The value of the label.
14
+ */
15
+ export function alertLabelValue(resource, label) {
16
+ return resource.labels[monitoringLabelName(label)];
17
+ }
18
+ /**
19
+ * Lists the alerts of a project that were open at a given time, along with their monitored resources.
20
+ *
21
+ * @param context The context, from which the `GoogleApisService` is obtained.
22
+ * @param project The GCP project.
23
+ * @param at The time at which the alerts were open.
24
+ * @returns The alerts.
25
+ */
26
+ export async function listOpenAlerts(context, project, at) {
27
+ const { GoogleApisService } = await import('../../services/google-apis.js');
28
+ const client = await context
29
+ .service(GoogleApisService)
30
+ .getClient('monitoring', 'v3', {});
31
+ // The API only supports `>=` and `<=` comparisons, and timestamps without fractional seconds. Alerts that are still
32
+ // open have no closing time.
33
+ const time = at.toISOString().replace(/\.\d+Z$/, 'Z');
34
+ const filter = `open_time <= "${time}" AND (state = "OPEN" OR close_time >= "${time}")`;
35
+ const alerts = [];
36
+ let pageToken;
37
+ do {
38
+ const { data } = await client.projects.alerts.list({
39
+ parent: `projects/${project}`,
40
+ filter,
41
+ pageSize: 1000,
42
+ pageToken,
43
+ });
44
+ alerts.push(...(data.alerts ?? []));
45
+ pageToken = data.nextPageToken ?? undefined;
46
+ } while (pageToken);
47
+ return alerts.map((alert) => {
48
+ const labels = {
49
+ ...alert.metric?.labels,
50
+ ...alert.resource?.labels,
51
+ };
52
+ const resourceType = alert.resource?.type;
53
+ // PromQL alerts have their resource type set to `prometheus_target`. In that case, the actual resource type is read
54
+ // from the `monitored_resource` label, which the alert is expected to group by.
55
+ const type = resourceType && resourceType !== PROMETHEUS_TARGET
56
+ ? resourceType
57
+ : labels.monitored_resource;
58
+ if (!type) {
59
+ return { alert, project };
60
+ }
61
+ return {
62
+ alert,
63
+ project,
64
+ resource: { type, project: labels.project_id ?? project, labels },
65
+ };
66
+ });
67
+ }