@causa/workspace-google 1.4.0-beta.1 → 1.4.0-beta.3
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 +19 -3
- package/dist/functions/event-topic/count-events-bigquery.call.d.ts +3 -0
- package/dist/functions/event-topic/count-events-bigquery.call.js +75 -0
- package/dist/functions/event-topic/count-events-bigquery.d.ts +11 -0
- package/dist/functions/event-topic/count-events-bigquery.js +19 -0
- package/dist/functions/event-topic/create-backfill-source-bigquery.call.js +6 -0
- package/dist/functions/event-topic/create-backfill-source-bigquery.d.ts +1 -1
- package/dist/functions/event-topic/create-backfill-source-bigquery.js +1 -1
- package/dist/functions/event-topic/index.d.ts +1 -0
- package/dist/functions/event-topic/index.js +1 -0
- package/dist/functions/event-topic/query-events-bigquery.call.js +2 -0
- package/dist/functions/event-topic/query-events-bigquery.d.ts +1 -0
- package/dist/functions/event-topic/query-events-bigquery.js +1 -0
- package/dist/functions/google-app-check/generate-token.js +3 -1
- package/dist/functions/google-firebase-storage/merge-rules.js +3 -1
- package/dist/functions/google-firestore/merge-rules.js +3 -1
- package/dist/functions/google-identity-platform/generate-custom-token.js +7 -2
- package/dist/functions/google-pubsub/write-topics.js +7 -2
- package/dist/functions/google-services/enable.js +3 -1
- package/dist/functions/google-spanner/count-records.call.d.ts +16 -0
- package/dist/functions/google-spanner/count-records.call.js +120 -0
- package/dist/functions/google-spanner/count-records.d.ts +15 -0
- package/dist/functions/google-spanner/count-records.js +21 -0
- package/dist/functions/google-spanner/index.d.ts +3 -1
- package/dist/functions/google-spanner/index.js +3 -1
- package/dist/functions/google-spanner/list-databases.d.ts +22 -1
- package/dist/functions/google-spanner/list-databases.js +27 -15
- package/dist/functions/google-spanner/resolve-table.d.ts +25 -0
- package/dist/functions/google-spanner/resolve-table.js +44 -0
- package/dist/functions/google-spanner/write-databases.js +7 -2
- package/dist/functions/graph/fetch-environment-metric-series.call.d.ts +3 -0
- package/dist/functions/graph/fetch-environment-metric-series.call.js +107 -0
- package/dist/functions/graph/fetch-environment-metric-series.d.ts +23 -0
- package/dist/functions/graph/fetch-environment-metric-series.js +40 -0
- package/dist/functions/graph/get-environment-provider.d.ts +11 -0
- package/dist/functions/graph/get-environment-provider.js +20 -0
- package/dist/functions/graph/index.d.ts +2 -0
- package/dist/functions/graph/index.js +2 -0
- package/dist/functions/index.js +4 -4
- package/dist/graph/cloud-run.d.ts +2 -2
- package/dist/graph/cloud-run.js +2 -2
- package/dist/graph/environment/fetchers/cloud-monitoring-alerts.d.ts +6 -0
- package/dist/graph/environment/fetchers/cloud-monitoring-alerts.js +106 -0
- package/dist/graph/environment/fetchers/cloud-monitoring.d.ts +5 -0
- package/dist/graph/environment/fetchers/cloud-monitoring.js +13 -0
- package/dist/graph/environment/fetchers/cloud-run.d.ts +5 -0
- package/dist/graph/environment/fetchers/cloud-run.js +88 -0
- package/dist/graph/environment/fetchers/cloud-scheduler.d.ts +5 -0
- package/dist/graph/environment/fetchers/cloud-scheduler.js +95 -0
- package/dist/graph/environment/fetchers/console-links.d.ts +14 -0
- package/dist/graph/environment/fetchers/console-links.js +132 -0
- package/dist/graph/environment/fetchers/error-reporting.d.ts +5 -0
- package/dist/graph/environment/fetchers/error-reporting.js +98 -0
- package/dist/graph/environment/fetchers/index.d.ts +5 -0
- package/dist/graph/environment/fetchers/index.js +17 -0
- package/dist/graph/environment/index.d.ts +9 -0
- package/dist/graph/environment/index.js +9 -0
- package/dist/graph/environment/load-balancer-backends.d.ts +82 -0
- package/dist/graph/environment/load-balancer-backends.js +126 -0
- package/dist/graph/environment/monitoring-alerts.d.ts +60 -0
- package/dist/graph/environment/monitoring-alerts.js +67 -0
- package/dist/graph/environment/monitoring-catalog.d.ts +95 -0
- package/dist/graph/environment/monitoring-catalog.js +356 -0
- package/dist/graph/environment/monitoring-mappings.d.ts +133 -0
- package/dist/graph/environment/monitoring-mappings.js +97 -0
- package/dist/graph/environment/monitoring-query.d.ts +116 -0
- package/dist/graph/environment/monitoring-query.js +314 -0
- package/dist/graph/environment/monitoring-values.d.ts +41 -0
- package/dist/graph/environment/monitoring-values.js +119 -0
- package/dist/graph/environment/resource-resolvers.d.ts +7 -0
- package/dist/graph/environment/resource-resolvers.js +57 -0
- package/dist/graph/environment/resources.d.ts +45 -0
- package/dist/graph/environment/resources.js +49 -0
- package/dist/graph/index.d.ts +1 -0
- package/dist/graph/index.js +1 -0
- package/dist/graph/rules/configuration/service-container.js +4 -4
- package/dist/graph/rules/terraform/spanner-databases.js +4 -4
- package/dist/graph/spanner.d.ts +11 -60
- package/dist/graph/spanner.js +25 -69
- package/package.json +24 -24
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type GraphBucketLayoutsByName, type GraphEnvironmentNodeOutput, type GraphMetricValue, type GraphWarning } from '@causa/workspace-core';
|
|
2
|
+
import { type GraphEnvironmentContext } from '@causa/workspace-core/graph';
|
|
3
|
+
import { type MonitoringMetric } from './monitoring-catalog.js';
|
|
4
|
+
import { BucketLayoutRegistry } from './monitoring-values.js';
|
|
5
|
+
/**
|
|
6
|
+
* The minimum alignment period accepted by Cloud Monitoring, in seconds.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MIN_ALIGNMENT_PERIOD = 60;
|
|
9
|
+
/**
|
|
10
|
+
* The delay after which Cloud Monitoring is expected to expose complete data, in seconds.
|
|
11
|
+
* Periods ending more recently may miss points, e.g. making rates look like 0.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MONITORING_DATA_DELAY = 300;
|
|
14
|
+
/**
|
|
15
|
+
* Returns the end of the periods over which metrics are read, which is at most {@link MONITORING_DATA_DELAY} before
|
|
16
|
+
* now.
|
|
17
|
+
*
|
|
18
|
+
* @param end The requested end.
|
|
19
|
+
* @returns The requested end, or {@link MONITORING_DATA_DELAY} before now if it is more recent.
|
|
20
|
+
*/
|
|
21
|
+
export declare function monitoringEnd(end: Date): Date;
|
|
22
|
+
/**
|
|
23
|
+
* The value of a metric of a node over one period, and the values of its groups when the metric is grouped.
|
|
24
|
+
*/
|
|
25
|
+
export type MonitoringPoint = Pick<GraphMetricValue, 'value' | 'groups'>;
|
|
26
|
+
/**
|
|
27
|
+
* The consecutive periods over which {@link queryMonitoringMetric} reads a metric.
|
|
28
|
+
*/
|
|
29
|
+
export type MonitoringPeriods = {
|
|
30
|
+
/**
|
|
31
|
+
* The end of the last period.
|
|
32
|
+
*/
|
|
33
|
+
readonly end: Date;
|
|
34
|
+
/**
|
|
35
|
+
* The length of each period, in seconds. It is at least {@link MIN_ALIGNMENT_PERIOD}.
|
|
36
|
+
*/
|
|
37
|
+
readonly period: number;
|
|
38
|
+
/**
|
|
39
|
+
* The number of periods, ending at {@link MonitoringPeriods.end}.
|
|
40
|
+
*/
|
|
41
|
+
readonly count: number;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The result of {@link queryMonitoringMetric}.
|
|
45
|
+
*/
|
|
46
|
+
export type MonitoringQueryResult = {
|
|
47
|
+
/**
|
|
48
|
+
* The values for each period, keyed by node ID. A period without data is `undefined`.
|
|
49
|
+
*/
|
|
50
|
+
readonly values: Map<string, (MonitoringPoint | undefined)[]>;
|
|
51
|
+
/**
|
|
52
|
+
* The names of the group keys of the values, when they are grouped.
|
|
53
|
+
*/
|
|
54
|
+
readonly groupBy?: string[];
|
|
55
|
+
/**
|
|
56
|
+
* The time series that could not be read, e.g. in a project. The values of the nodes they measure are missing.
|
|
57
|
+
*/
|
|
58
|
+
readonly errors: GraphWarning[];
|
|
59
|
+
/**
|
|
60
|
+
* Things worth reporting that don't affect the values, e.g. time series relating to resources missing from the graph.
|
|
61
|
+
*/
|
|
62
|
+
readonly warnings: GraphWarning[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Optional restrictions on the values returned by {@link queryMonitoringMetric}.
|
|
66
|
+
*/
|
|
67
|
+
export type MonitoringQueryOptions = {
|
|
68
|
+
/**
|
|
69
|
+
* The IDs of the nodes for which values should be returned. Defaults to all the nodes measured by the metric.
|
|
70
|
+
*/
|
|
71
|
+
readonly nodes?: readonly string[];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Reads the values of a metric over consecutive periods from Cloud Monitoring, for the nodes of the graph it measures.
|
|
75
|
+
* Time series are aligned over each period, and combined across the series of a node and group, within Cloud
|
|
76
|
+
* Monitoring. Time series are read once per GCP project, and only those of the project are kept.
|
|
77
|
+
* Metrics with {@link MonitoringMetric.groupByNode} mappings are also grouped by the nodes their time series relate to,
|
|
78
|
+
* unless one of the mappings could not be prepared. Time series relating to resources that are not in the graph are
|
|
79
|
+
* reported as warnings.
|
|
80
|
+
*
|
|
81
|
+
* @param environment The context of the environment, from which the Cloud Monitoring client and the resources of the
|
|
82
|
+
* graph are obtained.
|
|
83
|
+
* @param metric The metric to read.
|
|
84
|
+
* @param periods The periods over which the metric is read.
|
|
85
|
+
* @param layouts The registry naming the bucket layouts of distributions.
|
|
86
|
+
* @param options Restrictions on the nodes for which values are returned.
|
|
87
|
+
* @returns The values for each node and period.
|
|
88
|
+
*/
|
|
89
|
+
export declare function queryMonitoringMetric(environment: GraphEnvironmentContext, metric: MonitoringMetric, periods: MonitoringPeriods, layouts: BucketLayoutRegistry, options?: MonitoringQueryOptions): Promise<MonitoringQueryResult>;
|
|
90
|
+
/**
|
|
91
|
+
* The result of {@link queryPointInTimeMetrics}.
|
|
92
|
+
*/
|
|
93
|
+
export type PointInTimeMetrics = {
|
|
94
|
+
/**
|
|
95
|
+
* The values of the metrics, keyed by node ID.
|
|
96
|
+
*/
|
|
97
|
+
readonly nodes: Record<string, GraphEnvironmentNodeOutput>;
|
|
98
|
+
/**
|
|
99
|
+
* The bucket layouts referenced by the distributions.
|
|
100
|
+
*/
|
|
101
|
+
readonly bucketLayouts: GraphBucketLayoutsByName;
|
|
102
|
+
/**
|
|
103
|
+
* The warnings raised while reading the time series, or resolving the nodes of groups.
|
|
104
|
+
*/
|
|
105
|
+
readonly warnings: GraphWarning[];
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Reads the values of metrics over the evaluation window of an enrichment, for all the nodes of the graph, using
|
|
109
|
+
* {@link queryMonitoringMetric}.
|
|
110
|
+
* The window ends at most {@link MONITORING_DATA_DELAY} before now, such that it only covers complete data.
|
|
111
|
+
*
|
|
112
|
+
* @param environment The context of the enrichment.
|
|
113
|
+
* @param metrics The metrics to read.
|
|
114
|
+
* @returns The values of the metrics for each node.
|
|
115
|
+
*/
|
|
116
|
+
export declare function queryPointInTimeMetrics(environment: GraphEnvironmentContext, metrics: readonly MonitoringMetric[]): Promise<PointInTimeMetrics>;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { GraphMetricKind, } from '@causa/workspace-core';
|
|
2
|
+
import { sumGraphMetricMeasures, } from '@causa/workspace-core/graph';
|
|
3
|
+
import { monitoringMetricKind, } from './monitoring-catalog.js';
|
|
4
|
+
import { findMonitoredNode, monitoringLabelName, prepareMonitoringMapping, readMonitoringResourceKey, } from './monitoring-mappings.js';
|
|
5
|
+
import { BucketLayoutRegistry, toMeasure, toNumber, } from './monitoring-values.js';
|
|
6
|
+
import { googleResources } from './resources.js';
|
|
7
|
+
/**
|
|
8
|
+
* The gRPC code returned when a metric does not exist, e.g. because no data was ever written for it.
|
|
9
|
+
*/
|
|
10
|
+
const NOT_FOUND_CODE = 5;
|
|
11
|
+
/**
|
|
12
|
+
* How time series are aligned over a period, for each kind of metric.
|
|
13
|
+
* `ALIGN_RATE` returns a rate per second, `ALIGN_DELTA` the merged distribution over the period, and
|
|
14
|
+
* `ALIGN_NEXT_OLDER` the latest point before the end of the period.
|
|
15
|
+
*/
|
|
16
|
+
const ALIGNERS = {
|
|
17
|
+
[GraphMetricKind.Rate]: 'ALIGN_RATE',
|
|
18
|
+
[GraphMetricKind.Distribution]: 'ALIGN_DELTA',
|
|
19
|
+
[GraphMetricKind.Gauge]: 'ALIGN_NEXT_OLDER',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The minimum alignment period accepted by Cloud Monitoring, in seconds.
|
|
23
|
+
*/
|
|
24
|
+
export const MIN_ALIGNMENT_PERIOD = 60;
|
|
25
|
+
/**
|
|
26
|
+
* The delay after which Cloud Monitoring is expected to expose complete data, in seconds.
|
|
27
|
+
* Periods ending more recently may miss points, e.g. making rates look like 0.
|
|
28
|
+
*/
|
|
29
|
+
export const MONITORING_DATA_DELAY = 300;
|
|
30
|
+
/**
|
|
31
|
+
* Returns the end of the periods over which metrics are read, which is at most {@link MONITORING_DATA_DELAY} before
|
|
32
|
+
* now.
|
|
33
|
+
*
|
|
34
|
+
* @param end The requested end.
|
|
35
|
+
* @returns The requested end, or {@link MONITORING_DATA_DELAY} before now if it is more recent.
|
|
36
|
+
*/
|
|
37
|
+
export function monitoringEnd(end) {
|
|
38
|
+
return new Date(Math.min(end.getTime(), Date.now() - MONITORING_DATA_DELAY * 1000));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The group key whose values are the IDs of the nodes resolved from the {@link MonitoringMetric.groupByNode} mappings.
|
|
42
|
+
*/
|
|
43
|
+
const NODE_GROUP_KEY = 'node';
|
|
44
|
+
/**
|
|
45
|
+
* Returns the value of a label of a time series.
|
|
46
|
+
*
|
|
47
|
+
* @param series The time series.
|
|
48
|
+
* @param label The label, e.g. `resource.label.service_name` or `metric.label.response_code`.
|
|
49
|
+
* @returns The value of the label.
|
|
50
|
+
*/
|
|
51
|
+
function labelValue(series, label) {
|
|
52
|
+
const labels = label.startsWith('resource.')
|
|
53
|
+
? series.resource?.labels
|
|
54
|
+
: series.metric?.labels;
|
|
55
|
+
return labels?.[monitoringLabelName(label)] ?? undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Combines several measures of the same node and group.
|
|
59
|
+
*/
|
|
60
|
+
function combine(measures, reducer) {
|
|
61
|
+
if (reducer === 'REDUCE_MAX' &&
|
|
62
|
+
measures.every((m) => typeof m === 'number')) {
|
|
63
|
+
return Math.max(...measures);
|
|
64
|
+
}
|
|
65
|
+
return sumGraphMetricMeasures(measures);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns whether a measure is empty, i.e. 0 or a distribution without values.
|
|
69
|
+
*/
|
|
70
|
+
function isEmpty(measure) {
|
|
71
|
+
return typeof measure === 'number' ? measure === 0 : measure.count === 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Builds the point of a period from the measures of its groups.
|
|
75
|
+
* For rates and distributions, empty groups are dropped.
|
|
76
|
+
*
|
|
77
|
+
* @param byGroup The measures of the period, keyed by serialized group key.
|
|
78
|
+
* @param metric The metric.
|
|
79
|
+
* @param kind The kind of the values of the metric.
|
|
80
|
+
* @param grouped Whether the values of the metric are grouped.
|
|
81
|
+
* @returns The point, or `undefined` if all its groups are empty.
|
|
82
|
+
*/
|
|
83
|
+
function toPoint(byGroup, metric, kind, grouped) {
|
|
84
|
+
const reducer = metric.reducer ?? 'REDUCE_SUM';
|
|
85
|
+
const groups = [...byGroup.entries()]
|
|
86
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
87
|
+
.map(([, { key, measures }]) => ({
|
|
88
|
+
key,
|
|
89
|
+
value: combine(measures, reducer),
|
|
90
|
+
}))
|
|
91
|
+
.filter((g) => kind === GraphMetricKind.Gauge || !isEmpty(g.value));
|
|
92
|
+
if (groups.length === 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const value = combine(groups.map((g) => g.value), reducer);
|
|
96
|
+
return grouped ? { value, groups } : { value };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Reads the values of a metric over consecutive periods from Cloud Monitoring, for the nodes of the graph it measures.
|
|
100
|
+
* Time series are aligned over each period, and combined across the series of a node and group, within Cloud
|
|
101
|
+
* Monitoring. Time series are read once per GCP project, and only those of the project are kept.
|
|
102
|
+
* Metrics with {@link MonitoringMetric.groupByNode} mappings are also grouped by the nodes their time series relate to,
|
|
103
|
+
* unless one of the mappings could not be prepared. Time series relating to resources that are not in the graph are
|
|
104
|
+
* reported as warnings.
|
|
105
|
+
*
|
|
106
|
+
* @param environment The context of the environment, from which the Cloud Monitoring client and the resources of the
|
|
107
|
+
* graph are obtained.
|
|
108
|
+
* @param metric The metric to read.
|
|
109
|
+
* @param periods The periods over which the metric is read.
|
|
110
|
+
* @param layouts The registry naming the bucket layouts of distributions.
|
|
111
|
+
* @param options Restrictions on the nodes for which values are returned.
|
|
112
|
+
* @returns The values for each node and period.
|
|
113
|
+
*/
|
|
114
|
+
export async function queryMonitoringMetric(environment, metric, periods, layouts, options = {}) {
|
|
115
|
+
const { CloudMonitoringService } = await import('../../services/cloud-monitoring.js');
|
|
116
|
+
const { client } = environment.context.service(CloudMonitoringService);
|
|
117
|
+
// Calls on a gax client crash the process when credentials cannot be loaded, while `initialize` rejects.
|
|
118
|
+
await client.initialize();
|
|
119
|
+
const { mapping } = metric;
|
|
120
|
+
const { end, count } = periods;
|
|
121
|
+
const period = Math.max(Math.round(periods.period), MIN_ALIGNMENT_PERIOD);
|
|
122
|
+
const kind = monitoringMetricKind(metric);
|
|
123
|
+
const errors = [];
|
|
124
|
+
const nodes = options.nodes && new Set(options.nodes);
|
|
125
|
+
const measured = new Map(googleResources(environment, mapping.resourceType, mapping.nodeType)
|
|
126
|
+
.filter((r) => (!nodes || nodes.has(r.node.id)) &&
|
|
127
|
+
(!metric.measures || metric.measures(r.node, environment.graph)))
|
|
128
|
+
.map((r) => [r.name, r]));
|
|
129
|
+
// A mapping that could not be prepared is reported as a failure of the fact it depends on.
|
|
130
|
+
const primary = await prepareMonitoringMapping(environment, mapping);
|
|
131
|
+
if (!primary) {
|
|
132
|
+
return { values: new Map(), errors, warnings: [] };
|
|
133
|
+
}
|
|
134
|
+
// Values are only grouped by node when all the mappings are prepared. Otherwise, a group without node could either
|
|
135
|
+
// relate to a resource missing from the graph, or to one of a mapping that could not be prepared.
|
|
136
|
+
const nodeMappings = metric.groupByNode ?? [];
|
|
137
|
+
const preparedNodeMappings = await Promise.all(nodeMappings.map((m) => prepareMonitoringMapping(environment, m)));
|
|
138
|
+
const groupsByNode = nodeMappings.length > 0 && preparedNodeMappings.every((m) => !!m);
|
|
139
|
+
const nodeResolvers = groupsByNode
|
|
140
|
+
? preparedNodeMappings
|
|
141
|
+
: [];
|
|
142
|
+
// Time series relating to resources that are not in the graph are worth reporting, unlike the time series of other
|
|
143
|
+
// resources of the metric, which are expected.
|
|
144
|
+
const unmapped = new Set();
|
|
145
|
+
const groupByEntries = Object.entries(metric.groupBy ?? {});
|
|
146
|
+
const groupBy = [
|
|
147
|
+
...groupByEntries.map(([name]) => name),
|
|
148
|
+
...(groupsByNode ? [NODE_GROUP_KEY] : []),
|
|
149
|
+
];
|
|
150
|
+
const grouped = groupBy.length > 0;
|
|
151
|
+
const endSeconds = Math.floor(end.getTime() / 1000);
|
|
152
|
+
const request = {
|
|
153
|
+
interval: {
|
|
154
|
+
startTime: { seconds: endSeconds - count * period },
|
|
155
|
+
endTime: { seconds: endSeconds },
|
|
156
|
+
},
|
|
157
|
+
aggregation: {
|
|
158
|
+
alignmentPeriod: { seconds: period },
|
|
159
|
+
perSeriesAligner: metric.countRate ? 'ALIGN_DELTA' : ALIGNERS[kind],
|
|
160
|
+
crossSeriesReducer: metric.reducer ?? 'REDUCE_SUM',
|
|
161
|
+
groupByFields: [
|
|
162
|
+
...new Set([
|
|
163
|
+
...mapping.keyLabels,
|
|
164
|
+
...groupByEntries.map(([, label]) => label),
|
|
165
|
+
...nodeResolvers.flatMap(({ mapping }) => mapping.keyLabels),
|
|
166
|
+
]),
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
view: 'FULL',
|
|
170
|
+
};
|
|
171
|
+
const accumulated = new Map();
|
|
172
|
+
const succeededProjects = new Set();
|
|
173
|
+
const projects = new Set([...measured.values()].map((r) => r.segments.projects));
|
|
174
|
+
for (const project of projects) {
|
|
175
|
+
let series;
|
|
176
|
+
try {
|
|
177
|
+
[series] = await client.listTimeSeries({
|
|
178
|
+
...request,
|
|
179
|
+
name: `projects/${project}`,
|
|
180
|
+
// A project may be the scoping project of a metrics scope, whose time series include other projects'.
|
|
181
|
+
filter: [
|
|
182
|
+
`metric.type = "${metric.metricType}"`,
|
|
183
|
+
`resource.type = "${mapping.monitoredResource}"`,
|
|
184
|
+
`project = "${project}"`,
|
|
185
|
+
...(metric.filter ? [metric.filter] : []),
|
|
186
|
+
].join(' AND '),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (error.code === NOT_FOUND_CODE) {
|
|
191
|
+
succeededProjects.add(project);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
errors.push({
|
|
195
|
+
message: `Failed to read the metric '${metric.metricType}' in project '${project}': ${error.message ?? error}`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
succeededProjects.add(project);
|
|
201
|
+
for (const timeSeries of series) {
|
|
202
|
+
const read = (label) => labelValue(timeSeries, label);
|
|
203
|
+
const key = readMonitoringResourceKey(mapping, project, read);
|
|
204
|
+
const name = key && primary.resolve(key);
|
|
205
|
+
const node = name && measured.get(name)?.node.id;
|
|
206
|
+
if (!node) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const groupKey = {};
|
|
210
|
+
for (const [name, label] of groupByEntries) {
|
|
211
|
+
const value = labelValue(timeSeries, label);
|
|
212
|
+
if (value) {
|
|
213
|
+
groupKey[name] = value;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (groupsByNode) {
|
|
217
|
+
const { node: groupNode, missing } = findMonitoredNode(environment, nodeResolvers, project, read);
|
|
218
|
+
if (groupNode) {
|
|
219
|
+
groupKey[NODE_GROUP_KEY] = groupNode.id;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
missing.forEach(({ mapping, key, name }) => {
|
|
223
|
+
const description = Object.entries(key.labels)
|
|
224
|
+
.map(([label, value]) => `${label}=${value}`)
|
|
225
|
+
.sort()
|
|
226
|
+
.join(', ');
|
|
227
|
+
unmapped.add(`The '${mapping.monitoredResource}' time series with ${description} relate to '${name}', which is not in the graph.`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const serializedGroupKey = JSON.stringify(groupKey);
|
|
232
|
+
for (const point of timeSeries.points ?? []) {
|
|
233
|
+
const pointEnd = toNumber(point.interval?.endTime?.seconds);
|
|
234
|
+
const index = count - 1 - Math.round((endSeconds - pointEnd) / period);
|
|
235
|
+
if (index < 0 || index >= count || !point.value) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const byIndex = accumulated.get(node) ?? new Map();
|
|
239
|
+
accumulated.set(node, byIndex);
|
|
240
|
+
const byGroup = byIndex.get(index) ?? new Map();
|
|
241
|
+
byIndex.set(index, byGroup);
|
|
242
|
+
const group = byGroup.get(serializedGroupKey) ?? {
|
|
243
|
+
key: groupKey,
|
|
244
|
+
measures: [],
|
|
245
|
+
};
|
|
246
|
+
byGroup.set(serializedGroupKey, group);
|
|
247
|
+
group.measures.push(toMeasure(point.value, metric, period, layouts));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Rates are 0 when nothing happened during a period, in which case Cloud Monitoring returns no point, or only empty
|
|
252
|
+
// ones. Gauges and distributions without points are left missing, as there may simply be no sample during the period.
|
|
253
|
+
const zero = kind !== GraphMetricKind.Rate
|
|
254
|
+
? undefined
|
|
255
|
+
: grouped
|
|
256
|
+
? { value: 0, groups: [] }
|
|
257
|
+
: { value: 0 };
|
|
258
|
+
const values = new Map();
|
|
259
|
+
for (const { node, segments } of measured.values()) {
|
|
260
|
+
const byIndex = accumulated.get(node.id);
|
|
261
|
+
const succeeded = succeededProjects.has(segments.projects);
|
|
262
|
+
if (!byIndex && (!zero || !succeeded)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const points = Array.from({ length: count }, (_, index) => {
|
|
266
|
+
const byGroup = byIndex?.get(index);
|
|
267
|
+
const point = byGroup && toPoint(byGroup, metric, kind, grouped);
|
|
268
|
+
return point ?? (succeeded ? zero : undefined);
|
|
269
|
+
});
|
|
270
|
+
values.set(node.id, points);
|
|
271
|
+
}
|
|
272
|
+
const warnings = [...unmapped].sort().map((message) => ({ message }));
|
|
273
|
+
return { values, ...(grouped ? { groupBy } : {}), errors, warnings };
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Reads the values of metrics over the evaluation window of an enrichment, for all the nodes of the graph, using
|
|
277
|
+
* {@link queryMonitoringMetric}.
|
|
278
|
+
* The window ends at most {@link MONITORING_DATA_DELAY} before now, such that it only covers complete data.
|
|
279
|
+
*
|
|
280
|
+
* @param environment The context of the enrichment.
|
|
281
|
+
* @param metrics The metrics to read.
|
|
282
|
+
* @returns The values of the metrics for each node.
|
|
283
|
+
*/
|
|
284
|
+
export async function queryPointInTimeMetrics(environment, metrics) {
|
|
285
|
+
const layouts = new BucketLayoutRegistry();
|
|
286
|
+
const results = await Promise.all(metrics.map(async (metric) => ({
|
|
287
|
+
metric,
|
|
288
|
+
result: await queryMonitoringMetric(environment, metric, {
|
|
289
|
+
end: monitoringEnd(environment.at),
|
|
290
|
+
period: Math.max(environment.window, metric.minPeriod ?? 0),
|
|
291
|
+
count: 1,
|
|
292
|
+
}, layouts),
|
|
293
|
+
})));
|
|
294
|
+
const nodes = {};
|
|
295
|
+
for (const { metric, result } of results) {
|
|
296
|
+
for (const [node, [point]] of result.values) {
|
|
297
|
+
if (!point) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const { value, groups } = point;
|
|
301
|
+
const { groupBy } = result;
|
|
302
|
+
nodes[node] ??= { metrics: {} };
|
|
303
|
+
nodes[node].metrics[metric.metric] =
|
|
304
|
+
groups && groupBy ? { value, groupBy, groups } : { value };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Several metrics read from the same time series raise the same warnings, e.g. about resources not in the graph.
|
|
308
|
+
const messages = new Set(results.flatMap(({ result }) => [...result.errors, ...result.warnings].map((w) => w.message)));
|
|
309
|
+
return {
|
|
310
|
+
nodes,
|
|
311
|
+
bucketLayouts: layouts.all,
|
|
312
|
+
warnings: [...messages].map((message) => ({ message })),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { GraphBucketLayoutsByName, GraphMetricMeasure } from '@causa/workspace-core';
|
|
2
|
+
import type { protos } from '@google-cloud/monitoring';
|
|
3
|
+
import type { MonitoringMetric } from './monitoring-catalog.js';
|
|
4
|
+
type TypedValue = protos.google.monitoring.v3.ITypedValue;
|
|
5
|
+
/**
|
|
6
|
+
* Names the bucket layouts of distributions, such that distributions with the same bounds share a layout.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BucketLayoutRegistry {
|
|
9
|
+
/**
|
|
10
|
+
* The registered layouts, keyed by name.
|
|
11
|
+
*/
|
|
12
|
+
private readonly layouts;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the name of the layout with the given bounds, registering it if needed.
|
|
15
|
+
* Layouts are named after the metric type. Layouts of the same metric type with different bounds get a suffix.
|
|
16
|
+
*
|
|
17
|
+
* @param baseName The base name of the layout, e.g. the metric type.
|
|
18
|
+
* @param bounds The bounds of the layout.
|
|
19
|
+
* @returns The name of the layout.
|
|
20
|
+
*/
|
|
21
|
+
register(baseName: string, bounds: number[]): string;
|
|
22
|
+
/**
|
|
23
|
+
* The registered layouts, keyed by name.
|
|
24
|
+
*/
|
|
25
|
+
get all(): GraphBucketLayoutsByName;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Converts a protobuf number, which may be a string or a `Long` for 64-bit integers, to a number.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toNumber(value: unknown): number;
|
|
31
|
+
/**
|
|
32
|
+
* Converts the value of a point to a {@link GraphMetricMeasure}.
|
|
33
|
+
*
|
|
34
|
+
* @param value The value of the point.
|
|
35
|
+
* @param metric The metric.
|
|
36
|
+
* @param period The alignment period, in seconds.
|
|
37
|
+
* @param layouts The registry of layouts.
|
|
38
|
+
* @returns The measure.
|
|
39
|
+
*/
|
|
40
|
+
export declare function toMeasure(value: TypedValue, metric: MonitoringMetric, period: number, layouts: BucketLayoutRegistry): GraphMetricMeasure;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The number of significant digits to which the bounds of buckets are rounded.
|
|
3
|
+
* Computing bounds, e.g. `scale * growthFactor ** i`, leaves floating point noise in the last digits. Doubles guarantee
|
|
4
|
+
* 15 significant digits. This leaves a margin for accumulated errors, while keeping more precision than bounds need.
|
|
5
|
+
*/
|
|
6
|
+
const BOUND_SIGNIFICANT_DIGITS = 12;
|
|
7
|
+
/**
|
|
8
|
+
* Names the bucket layouts of distributions, such that distributions with the same bounds share a layout.
|
|
9
|
+
*/
|
|
10
|
+
export class BucketLayoutRegistry {
|
|
11
|
+
/**
|
|
12
|
+
* The registered layouts, keyed by name.
|
|
13
|
+
*/
|
|
14
|
+
layouts = {};
|
|
15
|
+
/**
|
|
16
|
+
* Returns the name of the layout with the given bounds, registering it if needed.
|
|
17
|
+
* Layouts are named after the metric type. Layouts of the same metric type with different bounds get a suffix.
|
|
18
|
+
*
|
|
19
|
+
* @param baseName The base name of the layout, e.g. the metric type.
|
|
20
|
+
* @param bounds The bounds of the layout.
|
|
21
|
+
* @returns The name of the layout.
|
|
22
|
+
*/
|
|
23
|
+
register(baseName, bounds) {
|
|
24
|
+
const serialized = JSON.stringify(bounds);
|
|
25
|
+
for (let index = 1;; index++) {
|
|
26
|
+
const name = index === 1 ? baseName : `${baseName}#${index}`;
|
|
27
|
+
const existing = this.layouts[name];
|
|
28
|
+
if (!existing) {
|
|
29
|
+
this.layouts[name] = bounds;
|
|
30
|
+
return name;
|
|
31
|
+
}
|
|
32
|
+
if (JSON.stringify(existing) === serialized) {
|
|
33
|
+
return name;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The registered layouts, keyed by name.
|
|
39
|
+
*/
|
|
40
|
+
get all() {
|
|
41
|
+
return { ...this.layouts };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Converts a protobuf number, which may be a string or a `Long` for 64-bit integers, to a number.
|
|
46
|
+
*/
|
|
47
|
+
export function toNumber(value) {
|
|
48
|
+
if (value === null || value === undefined) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
return typeof value === 'object' ? Number(value.toString()) : Number(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns the bounds of the buckets of a Cloud Monitoring distribution.
|
|
55
|
+
*
|
|
56
|
+
* @param distribution The distribution.
|
|
57
|
+
* @returns The bounds, or `undefined` if the bucket options are not supported.
|
|
58
|
+
*/
|
|
59
|
+
function distributionBounds(distribution) {
|
|
60
|
+
const { linearBuckets, exponentialBuckets, explicitBuckets } = distribution.bucketOptions ?? {};
|
|
61
|
+
if (linearBuckets) {
|
|
62
|
+
const { numFiniteBuckets, width, offset } = linearBuckets;
|
|
63
|
+
return Array.from({ length: (numFiniteBuckets ?? 0) + 1 }, (_, i) => (offset ?? 0) + (width ?? 0) * i);
|
|
64
|
+
}
|
|
65
|
+
if (exponentialBuckets) {
|
|
66
|
+
const { numFiniteBuckets, growthFactor, scale } = exponentialBuckets;
|
|
67
|
+
return Array.from({ length: (numFiniteBuckets ?? 0) + 1 }, (_, i) => (scale ?? 0) * (growthFactor ?? 0) ** i);
|
|
68
|
+
}
|
|
69
|
+
if (explicitBuckets?.bounds) {
|
|
70
|
+
return [...explicitBuckets.bounds];
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Converts a Cloud Monitoring distribution to a {@link GraphDistribution}.
|
|
76
|
+
*
|
|
77
|
+
* @param distribution The distribution.
|
|
78
|
+
* @param scale The factor applied to the values.
|
|
79
|
+
* @param layoutBaseName The base name of the layout.
|
|
80
|
+
* @param layouts The registry of layouts.
|
|
81
|
+
* @returns The distribution.
|
|
82
|
+
*/
|
|
83
|
+
function toGraphDistribution(distribution, scale, layoutBaseName, layouts) {
|
|
84
|
+
const bounds = (distributionBounds(distribution) ?? []).map((b) => Number((b * scale).toPrecision(BOUND_SIGNIFICANT_DIGITS)));
|
|
85
|
+
const layout = layouts.register(layoutBaseName, bounds);
|
|
86
|
+
const count = toNumber(distribution.count);
|
|
87
|
+
const buckets = {};
|
|
88
|
+
(distribution.bucketCounts ?? []).forEach((bucketCount, index) => {
|
|
89
|
+
const value = toNumber(bucketCount);
|
|
90
|
+
if (value > 0) {
|
|
91
|
+
buckets[index] = value;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
layout,
|
|
96
|
+
count,
|
|
97
|
+
...(count > 0 ? { mean: toNumber(distribution.mean) * scale } : {}),
|
|
98
|
+
buckets,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Converts the value of a point to a {@link GraphMetricMeasure}.
|
|
103
|
+
*
|
|
104
|
+
* @param value The value of the point.
|
|
105
|
+
* @param metric The metric.
|
|
106
|
+
* @param period The alignment period, in seconds.
|
|
107
|
+
* @param layouts The registry of layouts.
|
|
108
|
+
* @returns The measure.
|
|
109
|
+
*/
|
|
110
|
+
export function toMeasure(value, metric, period, layouts) {
|
|
111
|
+
const scale = metric.scale ?? 1;
|
|
112
|
+
if (value.distributionValue) {
|
|
113
|
+
if (metric.countRate) {
|
|
114
|
+
return toNumber(value.distributionValue.count) / period;
|
|
115
|
+
}
|
|
116
|
+
return toGraphDistribution(value.distributionValue, scale, metric.metricType, layouts);
|
|
117
|
+
}
|
|
118
|
+
return toNumber(value.doubleValue ?? value.int64Value) * scale;
|
|
119
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GraphResourcePrefixResolver } from '@causa/workspace-core';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the names of Cloud Tasks queues, whose name ends with a suffix that is not known from the workspace.
|
|
4
|
+
* Queues are listed once per project and location, and matched on the longest prefix, as a prefix may be the beginning
|
|
5
|
+
* of another one. At most one queue is expected to match a prefix.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CLOUD_TASKS_QUEUE_RESOLVER: GraphResourcePrefixResolver;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseResourceName } from './resources.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the names of Cloud Tasks queues, whose name ends with a suffix that is not known from the workspace.
|
|
4
|
+
* Queues are listed once per project and location, and matched on the longest prefix, as a prefix may be the beginning
|
|
5
|
+
* of another one. At most one queue is expected to match a prefix.
|
|
6
|
+
*/
|
|
7
|
+
export const CLOUD_TASKS_QUEUE_RESOLVER = {
|
|
8
|
+
resourceType: 'cloudtasks.googleapis.com/Queue',
|
|
9
|
+
resolve: async (context, prefixes) => {
|
|
10
|
+
const byParent = Map.groupBy(prefixes, ({ prefix }) => {
|
|
11
|
+
const segments = parseResourceName(prefix);
|
|
12
|
+
return `projects/${segments.projects}/locations/${segments.locations}`;
|
|
13
|
+
});
|
|
14
|
+
const { GoogleApisService } = await import('../../services/google-apis.js');
|
|
15
|
+
const client = await context
|
|
16
|
+
.service(GoogleApisService)
|
|
17
|
+
.getClient('cloudtasks', 'v2', {});
|
|
18
|
+
const ids = new Map();
|
|
19
|
+
const warnings = [];
|
|
20
|
+
await Promise.all([...byParent].map(async ([parent, prefixesOfParent]) => {
|
|
21
|
+
const queues = [];
|
|
22
|
+
try {
|
|
23
|
+
let pageToken;
|
|
24
|
+
do {
|
|
25
|
+
const { data } = await client.projects.locations.queues.list({
|
|
26
|
+
parent,
|
|
27
|
+
pageSize: 1000,
|
|
28
|
+
pageToken,
|
|
29
|
+
});
|
|
30
|
+
queues.push(...(data.queues ?? []).flatMap((q) => q.name ?? []));
|
|
31
|
+
pageToken = data.nextPageToken ?? undefined;
|
|
32
|
+
} while (pageToken);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
warnings.push({
|
|
36
|
+
message: `Failed to list the Cloud Tasks queues in '${parent}': ${error.message ?? error}`,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const sorted = prefixesOfParent
|
|
41
|
+
.map(({ prefix }) => prefix)
|
|
42
|
+
.sort((a, b) => b.length - a.length);
|
|
43
|
+
const matchesByPrefix = Map.groupBy(queues.filter((q) => sorted.some((p) => q.startsWith(p))), (q) => sorted.find((p) => q.startsWith(p)));
|
|
44
|
+
for (const { node, prefix } of prefixesOfParent) {
|
|
45
|
+
const matches = matchesByPrefix.get(prefix) ?? [];
|
|
46
|
+
if (matches.length !== 1) {
|
|
47
|
+
warnings.push({
|
|
48
|
+
message: `Found ${matches.length} Cloud Tasks queues starting with '${prefix}' for '${node}', instead of one.`,
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
ids.set(node, matches[0]);
|
|
53
|
+
}
|
|
54
|
+
}));
|
|
55
|
+
return { ids, warnings };
|
|
56
|
+
},
|
|
57
|
+
};
|