@emartech/aggregate-metric-logger 5.1.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/gcp-metric-collector/gcp-metric-collector.js +80 -49
- package/src/gcp-metric-collector/gcp-metric-collector.spec.js +87 -0
- package/src/gcp-time-series-builder/gcp-time-series-builder.js +42 -25
- package/src/gcp-time-series-builder/gcp-time-series-builder.spec.js +22 -1
- package/src/gcp-time-series-publisher/gcp-time-series-publisher.js +41 -26
- package/src/gcp-time-series-publisher/gcp-time-series-publisher.spec.js +15 -1
- package/src/index.js +1 -0
- package/test-helpers/test-helpers.js +1 -1
package/package.json
CHANGED
|
@@ -1,71 +1,102 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const stringify = require('json-stable-stringify');
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
4
|
+
const { createGcpTimeSeriesPublisher } = require('../gcp-time-series-publisher/gcp-time-series-publisher');
|
|
5
|
+
const { createGcpTimeSeriesBuilder } = require('../gcp-time-series-builder/gcp-time-series-builder');
|
|
6
6
|
const { createCounter } = require('../metric-types/counter/counter');
|
|
7
7
|
const { createDistribution } = require('../metric-types/distribution/distribution');
|
|
8
8
|
const { isNumber, isPlainObject } = require('lodash');
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
class GcpMetricCollector {
|
|
11
|
+
metrics = {};
|
|
12
|
+
initialized = false;
|
|
13
|
+
timeoutId = null;
|
|
14
|
+
projectId = null;
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error(`${ value } is not a number`);
|
|
16
|
+
constructor(projectId = null) {
|
|
17
|
+
this.projectId = projectId ?? process.env.MONITORING_PROJECT_ID;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
count(metricName, labels = {}, value = 1) {
|
|
21
|
+
if (!isNumber(value)) {
|
|
22
|
+
throw new Error(`${ value } is not a number`);
|
|
23
|
+
}
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
if (!isPlainObject(labels)) {
|
|
26
|
+
throw new Error('labels is not a plain object');
|
|
27
|
+
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
this.addToMetric(metricName, labels, 'counter', value);
|
|
30
|
+
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
measure(metricName, labels = {}, value = 1) {
|
|
33
|
+
return this.addToMetric(metricName, labels, 'distribution', value);
|
|
34
|
+
}
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
start(metricName, labels = {}) {
|
|
37
|
+
return { metricName, labels, start: Date.now() };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
stop(measurement) {
|
|
41
|
+
this.addToMetric(
|
|
42
|
+
measurement.metricName,
|
|
43
|
+
measurement.labels,
|
|
44
|
+
'distribution',
|
|
45
|
+
Date.now() - measurement.start
|
|
46
|
+
);
|
|
47
|
+
}
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
addToMetric(metricName, labels = {}, type, value) {
|
|
50
|
+
if (process.env.METRIC_LOGGER_ENABLED !== 'true') return;
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
if (!this.initialized) {
|
|
53
|
+
this.initialized = true;
|
|
54
|
+
this.setupNextFlush();
|
|
55
|
+
}
|
|
56
|
+
const key = `${metricName}_${stringify(labels)}`;
|
|
57
|
+
if (!this.metrics[key]) {
|
|
58
|
+
const aggregator = type === 'counter' ? createCounter() : createDistribution();
|
|
59
|
+
this.metrics[key] = { name: metricName, labels, aggregator };
|
|
60
|
+
}
|
|
61
|
+
this.metrics[key].aggregator.add(value);
|
|
45
62
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
|
|
64
|
+
reset() {
|
|
65
|
+
this.metrics = {};
|
|
66
|
+
this.initialized = false;
|
|
67
|
+
clearTimeout(this.timeoutId);
|
|
50
68
|
}
|
|
51
|
-
metrics[key].aggregator.add(value);
|
|
52
|
-
};
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
clearTimeout(timeoutId);
|
|
58
|
-
};
|
|
70
|
+
setupNextFlush() {
|
|
71
|
+
this.timeoutId = setTimeout(this.flush.bind(this), 60 * 1000);
|
|
72
|
+
}
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
async flush() {
|
|
75
|
+
const timeSeries = createGcpTimeSeriesBuilder(this.projectId).buildTimeSeries(this.metrics);
|
|
76
|
+
for (const [, metric] of Object.entries(this.metrics)) {
|
|
77
|
+
metric.aggregator.advanceTimer();
|
|
78
|
+
}
|
|
79
|
+
await createGcpTimeSeriesPublisher(this.projectId).publishTimeSeries(timeSeries);
|
|
80
|
+
this.setupNextFlush();
|
|
81
|
+
}
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (const [, metric] of Object.entries(metrics)) {
|
|
67
|
-
metric.aggregator.advanceTimer();
|
|
83
|
+
createGcpMetricsCollector(projectId) {
|
|
84
|
+
return new GcpMetricCollector(projectId);
|
|
68
85
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const collector = new GcpMetricCollector()
|
|
89
|
+
|
|
90
|
+
exports.count = (metricName, labels = {}, value = 1) =>
|
|
91
|
+
collector.count(metricName, labels, value);
|
|
92
|
+
|
|
93
|
+
exports.measure = (metricName, labels = {}, value = 1) =>
|
|
94
|
+
collector.measure(metricName, labels, value);
|
|
95
|
+
|
|
96
|
+
exports.start = (metricName, labels = {}) => collector.start(metricName, labels);
|
|
97
|
+
|
|
98
|
+
exports.stop = measurement => collector.stop(measurement);
|
|
99
|
+
|
|
100
|
+
exports.reset = () => collector.reset();
|
|
101
|
+
|
|
102
|
+
exports.createGcpMetricsCollector = projectId => new GcpMetricCollector(projectId)
|
|
@@ -33,6 +33,12 @@ describe('gcpMetricCollector', () => {
|
|
|
33
33
|
expect(getPublishedMetrics()).toEqual([
|
|
34
34
|
[
|
|
35
35
|
{
|
|
36
|
+
resource: {
|
|
37
|
+
labels: {
|
|
38
|
+
project_id: 'dummy-project'
|
|
39
|
+
},
|
|
40
|
+
type: 'global'
|
|
41
|
+
},
|
|
36
42
|
name: 'jolo',
|
|
37
43
|
labels: { kacsa: 'A' },
|
|
38
44
|
value: { int64Value: '88' },
|
|
@@ -45,6 +51,81 @@ describe('gcpMetricCollector', () => {
|
|
|
45
51
|
]);
|
|
46
52
|
});
|
|
47
53
|
|
|
54
|
+
it('should send metric for given gcp project id', () => {
|
|
55
|
+
jest.useFakeTimers().setSystemTime(new Date(20000));
|
|
56
|
+
stubCreateTimeSeries();
|
|
57
|
+
|
|
58
|
+
gcpMetricCollector.createGcpMetricsCollector('test-project').count('jolo', { kacsa: 'A' }, 88);
|
|
59
|
+
|
|
60
|
+
expect(getPublishedMetrics()).toEqual([]);
|
|
61
|
+
|
|
62
|
+
jest.advanceTimersByTime(60 * 1000 + 1);
|
|
63
|
+
|
|
64
|
+
expect(getPublishedMetrics()).toEqual([
|
|
65
|
+
[
|
|
66
|
+
expect.objectContaining({
|
|
67
|
+
resource: {
|
|
68
|
+
labels: {
|
|
69
|
+
project_id: 'test-project'
|
|
70
|
+
},
|
|
71
|
+
type: 'global'
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
]
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should measure seperate metrics for each gcp projects', () => {
|
|
79
|
+
jest.useFakeTimers().setSystemTime(new Date(20000));
|
|
80
|
+
stubCreateTimeSeries();
|
|
81
|
+
|
|
82
|
+
const anotherCollector = gcpMetricCollector.createGcpMetricsCollector('another-project');
|
|
83
|
+
|
|
84
|
+
gcpMetricCollector.count('jolo', { kacsa: 'A' }, 88);
|
|
85
|
+
anotherCollector.count('jolo', { kacsa: 'A' }, 66);
|
|
86
|
+
|
|
87
|
+
expect(getPublishedMetrics()).toEqual([]);
|
|
88
|
+
|
|
89
|
+
jest.advanceTimersByTime(60 * 1000 + 1);
|
|
90
|
+
|
|
91
|
+
expect(getPublishedMetrics()).toEqual([
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
resource: {
|
|
95
|
+
labels: {
|
|
96
|
+
project_id: 'dummy-project'
|
|
97
|
+
},
|
|
98
|
+
type: 'global'
|
|
99
|
+
},
|
|
100
|
+
name: 'jolo',
|
|
101
|
+
labels: { kacsa: 'A' },
|
|
102
|
+
value: { int64Value: '88' },
|
|
103
|
+
startTime: 19000,
|
|
104
|
+
endTime: 80000,
|
|
105
|
+
metricKind: 'CUMULATIVE',
|
|
106
|
+
valueType: 'int64Value'
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
resource: {
|
|
112
|
+
labels: {
|
|
113
|
+
project_id: 'another-project'
|
|
114
|
+
},
|
|
115
|
+
type: 'global'
|
|
116
|
+
},
|
|
117
|
+
name: 'jolo',
|
|
118
|
+
labels: { kacsa: 'A' },
|
|
119
|
+
value: { int64Value: '66' },
|
|
120
|
+
startTime: 19000,
|
|
121
|
+
endTime: 80000,
|
|
122
|
+
metricKind: 'CUMULATIVE',
|
|
123
|
+
valueType: 'int64Value'
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
]);
|
|
127
|
+
})
|
|
128
|
+
|
|
48
129
|
it('should sum up counts that happened and publish the sum, send a metric for each label combo', () => {
|
|
49
130
|
stubCreateTimeSeries();
|
|
50
131
|
|
|
@@ -134,6 +215,12 @@ describe('gcpMetricCollector', () => {
|
|
|
134
215
|
labels: { kacsa: 'A' },
|
|
135
216
|
startTime: 80000,
|
|
136
217
|
endTime: 80000,
|
|
218
|
+
resource: {
|
|
219
|
+
labels: {
|
|
220
|
+
project_id: 'dummy-project'
|
|
221
|
+
},
|
|
222
|
+
type: 'global'
|
|
223
|
+
},
|
|
137
224
|
value: {
|
|
138
225
|
distributionValue: {
|
|
139
226
|
count: 1,
|
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
class GcpTimeSeriesBuilder {
|
|
4
|
+
projectId = null;
|
|
5
|
+
|
|
6
|
+
constructor(projectId = null) {
|
|
7
|
+
this.projectId = projectId ?? process.env.MONITORING_PROJECT_ID;
|
|
8
|
+
}
|
|
9
|
+
buildTimeSeries(metrics) {
|
|
10
|
+
return Object.values(metrics).map(metric => {
|
|
11
|
+
const state = metric.aggregator.getState();
|
|
12
|
+
return {
|
|
13
|
+
metric: {
|
|
14
|
+
type: `custom.googleapis.com/${metric.name}`,
|
|
15
|
+
labels: { ...metric.labels, hostname: process.env.HOSTNAME }
|
|
16
|
+
},
|
|
17
|
+
resource: {
|
|
18
|
+
type: 'global',
|
|
19
|
+
labels: { project_id: this.projectId }
|
|
20
|
+
},
|
|
21
|
+
points: [
|
|
22
|
+
{
|
|
23
|
+
interval: {
|
|
24
|
+
startTime: { seconds: state.startTime / 1000 },
|
|
25
|
+
endTime: { seconds: state.endTime / 1000 }
|
|
26
|
+
},
|
|
27
|
+
value: state.value
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
metricKind: state.metricKind,
|
|
31
|
+
valueType: state.valueType
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
exports.createGcpTimeSeriesBuilder = projectId => {
|
|
38
|
+
return new GcpTimeSeriesBuilder(projectId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
exports.buildTimeSeries = metrics => {
|
|
42
|
+
const builder = new GcpTimeSeriesBuilder();
|
|
43
|
+
return builder.buildTimeSeries(metrics);
|
|
44
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { buildTimeSeries } = require('./gcp-time-series-builder');
|
|
3
|
+
const { buildTimeSeries, createGcpTimeSeriesBuilder } = require('./gcp-time-series-builder');
|
|
4
4
|
|
|
5
5
|
jest.mock('@google-cloud/monitoring');
|
|
6
6
|
|
|
@@ -69,6 +69,27 @@ describe('gcpTimeSeriesBuilder', () => {
|
|
|
69
69
|
]);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it('should create time series from metric for given gcp project id', async () => {
|
|
73
|
+
const metrics = {
|
|
74
|
+
key1: dummyMetric({ name: 'kacs', labels: { a: 1 }, count: 14 })
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect(createGcpTimeSeriesBuilder('test-project').buildTimeSeries(metrics)).toEqual([
|
|
78
|
+
{
|
|
79
|
+
metric: expect.anything(),
|
|
80
|
+
metricKind: expect.anything(),
|
|
81
|
+
points: expect.anything(),
|
|
82
|
+
resource: {
|
|
83
|
+
labels: {
|
|
84
|
+
project_id: 'test-project'
|
|
85
|
+
},
|
|
86
|
+
type: 'global'
|
|
87
|
+
},
|
|
88
|
+
valueType: expect.anything()
|
|
89
|
+
}
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
72
93
|
it('should create time series from all the metrics', async () => {
|
|
73
94
|
const metrics = {
|
|
74
95
|
key1: dummyMetric({ name: 'kacs', labels: { a: 1 }, count: 14 }),
|
|
@@ -8,33 +8,48 @@ const { setTimeout } = require('timers/promises');
|
|
|
8
8
|
|
|
9
9
|
const MAX_TIME_SERIES_COUNT = 200;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
for (const timeSeriesChunk of chunk(timeSeries, MAX_TIME_SERIES_COUNT)) {
|
|
15
|
-
await createTimeSeries(client, timeSeriesChunk);
|
|
16
|
-
}
|
|
17
|
-
} catch (error) {
|
|
18
|
-
logger.warnFromError('pushing-metrics-failed', error, { tags: ['alert_never'] });
|
|
19
|
-
}
|
|
20
|
-
};
|
|
11
|
+
class GcpTimeSeriesPublisher {
|
|
12
|
+
monitoringClientInstance = null;
|
|
13
|
+
projectId = null;
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
await client.createTimeSeries({ name, timeSeries });
|
|
26
|
-
} catch (error) {
|
|
27
|
-
await setTimeout(ms('10 sec'));
|
|
28
|
-
await client.createTimeSeries({ name, timeSeries });
|
|
15
|
+
constructor(projectId = null) {
|
|
16
|
+
this.projectId = projectId ?? process.env.MONITORING_PROJECT_ID;
|
|
29
17
|
}
|
|
30
|
-
};
|
|
31
18
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
19
|
+
async publishTimeSeries(timeSeries) {
|
|
20
|
+
try {
|
|
21
|
+
const client = this.getMonitoringClient();
|
|
22
|
+
for (const timeSeriesChunk of chunk(timeSeries, MAX_TIME_SERIES_COUNT)) {
|
|
23
|
+
await this.createTimeSeries(client, timeSeriesChunk);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.warnFromError('pushing-metrics-failed', error, { tags: ['alert_never'] });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async createTimeSeries(client, timeSeries) {
|
|
31
|
+
const name = client.projectPath(this.projectId);
|
|
32
|
+
try {
|
|
33
|
+
await client.createTimeSeries({ name, timeSeries });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
await setTimeout(ms('10 sec'));
|
|
36
|
+
await client.createTimeSeries({ name, timeSeries });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
getMonitoringClient() {
|
|
41
|
+
if (!this.monitoringClientInstance) {
|
|
42
|
+
this.monitoringClientInstance = new monitoring.MetricServiceClient({
|
|
43
|
+
projectId: this.projectId
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return this.monitoringClientInstance;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.createGcpTimeSeriesPublisher = projectId => new GcpTimeSeriesPublisher(projectId)
|
|
51
|
+
|
|
52
|
+
exports.publishTimeSeries = async timeSeries => {
|
|
53
|
+
const publisher = new GcpTimeSeriesPublisher()
|
|
54
|
+
await publisher.publishTimeSeries(timeSeries);
|
|
40
55
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { MetricServiceClient } = require('@google-cloud/monitoring');
|
|
4
4
|
const { range } = require('lodash');
|
|
5
|
-
const { publishTimeSeries } = require('./gcp-time-series-publisher');
|
|
5
|
+
const { publishTimeSeries, createGcpTimeSeriesPublisher } = require('./gcp-time-series-publisher');
|
|
6
6
|
const { stubCreateTimeSeries } = require('../../test-helpers/test-helpers');
|
|
7
7
|
|
|
8
8
|
jest.mock('@google-cloud/monitoring');
|
|
@@ -23,6 +23,20 @@ describe('gcpMetricPublisher', () => {
|
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
it('should push metrics to gcp to given project', async () => {
|
|
27
|
+
stubCreateTimeSeries();
|
|
28
|
+
process.env.HOSTNAME = 'test-host';
|
|
29
|
+
process.env.MONITORING_PROJECT_ID = 'test-project-1';
|
|
30
|
+
const timeSeries = [1, 2];
|
|
31
|
+
|
|
32
|
+
await createGcpTimeSeriesPublisher('test-project-2').publishTimeSeries(timeSeries);
|
|
33
|
+
|
|
34
|
+
expect(MetricServiceClient.prototype.createTimeSeries).toHaveBeenCalledWith({
|
|
35
|
+
name: 'projects/test-project-2',
|
|
36
|
+
timeSeries
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
26
40
|
it('should push metrics to gcp in batches', async () => {
|
|
27
41
|
stubCreateTimeSeries();
|
|
28
42
|
process.env.HOSTNAME = 'test-host';
|
package/src/index.js
CHANGED
|
@@ -26,5 +26,6 @@ exposedFactoryInstanceHybrid.count = gcpMetricCollector.count;
|
|
|
26
26
|
exposedFactoryInstanceHybrid.measureGcp = gcpMetricCollector.measure;
|
|
27
27
|
exposedFactoryInstanceHybrid.startGcp = gcpMetricCollector.start;
|
|
28
28
|
exposedFactoryInstanceHybrid.stopGcp = gcpMetricCollector.stop;
|
|
29
|
+
exposedFactoryInstanceHybrid.createGcpMetricsCollector = gcpMetricCollector.createGcpMetricsCollector;
|
|
29
30
|
|
|
30
31
|
module.exports = exposedFactoryInstanceHybrid;
|
|
@@ -14,7 +14,7 @@ exports.getPublishedMetrics = () => {
|
|
|
14
14
|
|
|
15
15
|
return mock.calls.map(call =>
|
|
16
16
|
call[0].timeSeries.map(series => ({
|
|
17
|
-
...pick(series, ['metricKind', 'valueType']),
|
|
17
|
+
...pick(series, ['metricKind', 'valueType', 'resource']),
|
|
18
18
|
name: series.metric.type.replace('custom.googleapis.com/', ''),
|
|
19
19
|
labels: omit(series.metric.labels, 'hostname'),
|
|
20
20
|
startTime: series.points[0].interval.startTime.seconds * 1000,
|