@emartech/aggregate-metric-logger 2.1.3 → 2.2.0

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/.eslintrc.js CHANGED
@@ -11,7 +11,8 @@ module.exports = {
11
11
  },
12
12
  extends: ['eslint:recommended', 'plugin:jest/recommended', 'emarsys'],
13
13
  rules: {
14
- 'curly': ['error', 'multi-line']
14
+ curly: ['error', 'multi-line'],
15
+ 'operator-linebreak': ['error', 'after', { overrides: { '?': 'before', ':': 'before' } }]
15
16
  },
16
17
  overrides: [
17
18
  {
package/README.md CHANGED
@@ -11,7 +11,7 @@ To change the default `aggregate-metric-logger` namespace used for logs, set
11
11
 
12
12
  ## Setup to send metrics to GCP monitoring
13
13
 
14
- (currently only the `metricLogger.count` method sends metrics to GCP)
14
+ (currently only the `metricLogger.count`, `metricLogger.measureGcp` method sends metrics to GCP)
15
15
 
16
16
  Set `METRIC_LOGGER_ENABLED` just us for LaaS based metrics.
17
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emartech/aggregate-metric-logger",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "aggregate metrics in memory for logging",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const stringify = require('json-stable-stringify');
4
+ const { publishTimeSeries } = require('../gcp-time-series-publisher/gcp-time-series-publisher');
5
+ const { buildTimeSeries } = require('../gcp-time-series-builder/gcp-time-series-builder');
6
+ const { createCounter } = require('../metric-types/counter/counter');
7
+ const { createDistribution } = require('../metric-types/distribution/distribution');
8
+
9
+ let metrics = {};
10
+ let initialized = false;
11
+ let timeoutId = null;
12
+
13
+ exports.count = (metricName, labels = {}, value = 1) =>
14
+ addToMetric(metricName, labels, 'counter', value);
15
+
16
+ exports.measure = (metricName, labels = {}, value = 1) =>
17
+ addToMetric(metricName, labels, 'distribution', value);
18
+
19
+ const addToMetric = (metricName, labels = {}, type, value) => {
20
+ if (process.env.METRIC_LOGGER_ENABLED !== 'true') return;
21
+
22
+ if (!initialized) {
23
+ initialized = true;
24
+ setupNextFlush();
25
+ }
26
+ const key = `${metricName}_${stringify(labels)}`;
27
+ if (!metrics[key]) {
28
+ const value = type === 'counter' ? createCounter() : createDistribution();
29
+ metrics[key] = { name: metricName, labels, value };
30
+ }
31
+ metrics[key].value.add(value);
32
+ };
33
+
34
+ exports.reset = () => {
35
+ metrics = {};
36
+ initialized = false;
37
+ clearTimeout(timeoutId);
38
+ };
39
+
40
+ const setupNextFlush = () => {
41
+ timeoutId = setTimeout(flush, 60 * 1000);
42
+ };
43
+
44
+ const flush = async () => {
45
+ const timeSeries = buildTimeSeries(metrics);
46
+ for (const [, metric] of Object.entries(metrics)) {
47
+ metric.value.advanceTimer();
48
+ }
49
+ await publishTimeSeries(timeSeries);
50
+ setupNextFlush();
51
+ };
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ const gcpMetricCollector = require('./gcp-metric-collector');
4
+ const {
5
+ stubCreateTimeSeries,
6
+ getPublishedMetrics,
7
+ flushPromises,
8
+ stubCreateTimeSeriesWithCallbackDuringPublish
9
+ } = require('../../test-helpers/test-helpers');
10
+
11
+ beforeEach(() => {
12
+ jest.useFakeTimers();
13
+ gcpMetricCollector.reset();
14
+ });
15
+ afterEach(() => {
16
+ jest.useRealTimers();
17
+ gcpMetricCollector.reset();
18
+ });
19
+
20
+ describe('gcpMetricCollector', () => {
21
+ describe('count', () => {
22
+ it('should send metric for a single measurement in next flush period', () => {
23
+ jest.useFakeTimers().setSystemTime(new Date(20000));
24
+ stubCreateTimeSeries();
25
+
26
+ gcpMetricCollector.count('jolo', { kacsa: 'A' }, 88);
27
+
28
+ expect(getPublishedMetrics()).toEqual([]);
29
+
30
+ jest.advanceTimersByTime(60 * 1000 + 1);
31
+
32
+ expect(getPublishedMetrics()).toEqual([
33
+ [
34
+ {
35
+ name: 'jolo',
36
+ labels: { kacsa: 'A' },
37
+ value: { int64Value: '88' },
38
+ startTime: 20000,
39
+ endTime: 80000,
40
+ metricKind: 'CUMULATIVE',
41
+ valueType: 'int64Value'
42
+ }
43
+ ]
44
+ ]);
45
+ });
46
+
47
+ it('should sum up counts that happened and publish the sum, send a metric for each label combo', () => {
48
+ stubCreateTimeSeries();
49
+
50
+ gcpMetricCollector.count('jolo', { a: 1 });
51
+ gcpMetricCollector.count('jolo');
52
+ gcpMetricCollector.count('yolo');
53
+ gcpMetricCollector.count('yolo');
54
+ gcpMetricCollector.count('jolo');
55
+
56
+ expect(getPublishedMetrics()).toEqual([]);
57
+
58
+ jest.advanceTimersByTime(60 * 1000 + 1);
59
+
60
+ expect(getPublishedMetrics()).toMatchObject([
61
+ [
62
+ { name: 'jolo', labels: { a: 1 }, value: { int64Value: '1' } },
63
+ { name: 'jolo', labels: {}, value: { int64Value: '2' } },
64
+ { name: 'yolo', labels: {}, value: { int64Value: '2' } }
65
+ ]
66
+ ]);
67
+ });
68
+
69
+ it('should not reset counts after sending metrics, but keep increasing them', async () => {
70
+ stubCreateTimeSeries();
71
+
72
+ gcpMetricCollector.count('jolo');
73
+ gcpMetricCollector.count('jolo');
74
+
75
+ jest.advanceTimersByTime(60 * 1000 + 1);
76
+ await flushPromises();
77
+ expect(getPublishedMetrics()).toMatchObject([
78
+ [{ name: 'jolo', labels: {}, value: { int64Value: '2' } }]
79
+ ]);
80
+
81
+ gcpMetricCollector.count('jolo');
82
+ jest.advanceTimersByTime(60 * 1000 + 1);
83
+ await flushPromises();
84
+
85
+ expect(getPublishedMetrics()).toMatchObject([
86
+ [{ name: 'jolo', labels: {}, value: { int64Value: '2' } }],
87
+ [{ name: 'jolo', labels: {}, value: { int64Value: '3' } }]
88
+ ]);
89
+ });
90
+
91
+ it('should not publish metrics if metric logger is not enabled', async () => {
92
+ process.env.METRIC_LOGGER_ENABLED = 'false';
93
+ stubCreateTimeSeries();
94
+
95
+ gcpMetricCollector.count('jolo', { kacsa: 'A' }, 88);
96
+ jest.advanceTimersByTime(60 * 1000 + 1);
97
+
98
+ expect(getPublishedMetrics()).toEqual([]);
99
+ });
100
+ });
101
+
102
+ describe('measure', () => {
103
+ it('should send metric for a single measurement in next flush period', () => {
104
+ jest.useFakeTimers().setSystemTime(new Date(20000));
105
+ stubCreateTimeSeries();
106
+
107
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 7);
108
+
109
+ expect(getPublishedMetrics()).toEqual([]);
110
+
111
+ jest.advanceTimersByTime(60 * 1000);
112
+
113
+ expect(getPublishedMetrics()).toEqual([
114
+ [
115
+ {
116
+ name: 'jolo',
117
+ labels: { kacsa: 'A' },
118
+ startTime: 80000,
119
+ endTime: 80000,
120
+ value: {
121
+ distributionValue: {
122
+ count: 1,
123
+ mean: 7,
124
+ sumOfSquaredDeviation: 0,
125
+ bucketOptions: {
126
+ exponentialBuckets: { numFiniteBuckets: 20, growthFactor: 2, scale: 1 }
127
+ },
128
+ bucketCounts: [0, 0, 0, 1, ...Array(18).fill(0)]
129
+ }
130
+ },
131
+ metricKind: 'GAUGE',
132
+ valueType: 'DISTRIBUTION'
133
+ }
134
+ ]
135
+ ]);
136
+ });
137
+
138
+ it('should calculate mean and sum of squared deviation', async () => {
139
+ stubCreateTimeSeries();
140
+
141
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 7);
142
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 12);
143
+
144
+ jest.advanceTimersByTime(60 * 1000 + 1);
145
+ await flushPromises();
146
+ expect(getPublishedMetrics()).toMatchObject([
147
+ [
148
+ {
149
+ value: {
150
+ distributionValue: {
151
+ count: 2,
152
+ mean: 9.5,
153
+ sumOfSquaredDeviation: 12.5,
154
+ bucketCounts: [0, 0, 0, 1, 1, ...Array(17).fill(0)]
155
+ }
156
+ }
157
+ }
158
+ ]
159
+ ]);
160
+ });
161
+
162
+ it('should reset distribution value after sending metrics', async () => {
163
+ stubCreateTimeSeries();
164
+
165
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 7);
166
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 12);
167
+
168
+ jest.advanceTimersByTime(60 * 1000 + 1);
169
+ await flushPromises();
170
+
171
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 5);
172
+
173
+ jest.advanceTimersByTime(60 * 1000 + 1);
174
+ await flushPromises();
175
+
176
+ expect(getPublishedMetrics()).toMatchObject([
177
+ [{}],
178
+ [
179
+ {
180
+ value: {
181
+ distributionValue: {
182
+ count: 1,
183
+ mean: 5,
184
+ sumOfSquaredDeviation: 0,
185
+ bucketCounts: [0, 0, 0, 1, ...Array(18).fill(0)]
186
+ }
187
+ }
188
+ }
189
+ ]
190
+ ]);
191
+ });
192
+
193
+ it('should not remove metrics data during publishing', async () => {
194
+ stubCreateTimeSeriesWithCallbackDuringPublish(() => gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 5));
195
+
196
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 7);
197
+
198
+ jest.advanceTimersByTime(60 * 1000 + 1);
199
+ await flushPromises();
200
+ jest.advanceTimersByTime(60 * 1000 + 1);
201
+ await flushPromises();
202
+
203
+ expect(getPublishedMetrics()).toMatchObject([
204
+ [{}],
205
+ [
206
+ {
207
+ value: {
208
+ distributionValue: {
209
+ count: 1,
210
+ mean: 5,
211
+ sumOfSquaredDeviation: 0,
212
+ bucketCounts: [0, 0, 0, 1, ...Array(18).fill(0)]
213
+ }
214
+ }
215
+ }
216
+ ]
217
+ ]);
218
+ });
219
+
220
+ it('should not publish metrics if metric logger is not enabled', async () => {
221
+ process.env.METRIC_LOGGER_ENABLED = 'false';
222
+ stubCreateTimeSeries();
223
+
224
+ gcpMetricCollector.measure('jolo', { kacsa: 'A' }, 88);
225
+ jest.advanceTimersByTime(60 * 1000 + 1);
226
+
227
+ expect(getPublishedMetrics()).toEqual([]);
228
+ });
229
+ });
230
+ });
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ exports.buildTimeSeries = metrics =>
4
+ Object.values(metrics).map(metric => {
5
+ const state = metric.value.getState();
6
+ return {
7
+ metric: {
8
+ type: `custom.googleapis.com/${metric.name}`,
9
+ labels: { ...metric.labels, hostname: process.env.HOSTNAME }
10
+ },
11
+ resource: {
12
+ type: 'global',
13
+ labels: { project_id: process.env.GCP_PROJECT_ID }
14
+ },
15
+ points: [
16
+ {
17
+ interval: {
18
+ startTime: { seconds: state.startTime / 1000 },
19
+ endTime: { seconds: state.endTime / 1000 }
20
+ },
21
+ value: state.value
22
+ }
23
+ ],
24
+ metricKind: state.metricKind,
25
+ valueType: state.valueType
26
+ };
27
+ });
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const { buildTimeSeries } = require('./gcp-time-series-builder');
4
+
5
+ jest.mock('@google-cloud/monitoring');
6
+
7
+ const dummyMetric = ({
8
+ name = 'kacs',
9
+ labels = { a: 1 },
10
+ count = 14,
11
+ startTime = 15000,
12
+ endTime = 80000,
13
+ metricKind = 'CUMULATIVE',
14
+ valueType = 'int64Value'
15
+ }) => ({
16
+ name,
17
+ labels,
18
+ value: {
19
+ getState: () => ({
20
+ value: { int64Value: `${count}` },
21
+ startTime,
22
+ endTime,
23
+ valueType,
24
+ metricKind
25
+ })
26
+ }
27
+ });
28
+
29
+ describe('gcpTimeSeriesBuilder', () => {
30
+ describe('buildTimeSeries', () => {
31
+ it('should create time series from metric', async () => {
32
+ const metrics = {
33
+ key1: dummyMetric({ name: 'kacs', labels: { a: 1 }, count: 14 })
34
+ };
35
+
36
+ expect(buildTimeSeries(metrics)).toEqual([
37
+ {
38
+ metric: {
39
+ labels: {
40
+ a: 1,
41
+ hostname: 'local-machine'
42
+ },
43
+ type: 'custom.googleapis.com/kacs'
44
+ },
45
+ metricKind: 'CUMULATIVE',
46
+ points: [
47
+ {
48
+ interval: {
49
+ endTime: {
50
+ seconds: 80
51
+ },
52
+ startTime: {
53
+ seconds: 15
54
+ }
55
+ },
56
+ value: {
57
+ int64Value: '14'
58
+ }
59
+ }
60
+ ],
61
+ resource: {
62
+ labels: {
63
+ project_id: 'dummy-project'
64
+ },
65
+ type: 'global'
66
+ },
67
+ valueType: 'int64Value'
68
+ }
69
+ ]);
70
+ });
71
+
72
+ it('should create time series from all the metrics', async () => {
73
+ const metrics = {
74
+ key1: dummyMetric({ name: 'kacs', labels: { a: 1 }, count: 14 }),
75
+ key2: dummyMetric({ name: 'kacs', labels: { a: 2 }, count: 88 }),
76
+ key3: dummyMetric({ name: 'lacs', labels: {}, count: 1 })
77
+ };
78
+
79
+ expect(buildTimeSeries(metrics)).toMatchObject([
80
+ {
81
+ points: [{ value: { int64Value: '14' } }],
82
+ metric: {
83
+ type: 'custom.googleapis.com/kacs',
84
+ labels: { a: 1 }
85
+ }
86
+ },
87
+ {
88
+ points: [{ value: { int64Value: '88' } }],
89
+ metric: {
90
+ type: 'custom.googleapis.com/kacs',
91
+ labels: { a: 2 }
92
+ }
93
+ },
94
+ {
95
+ points: [{ value: { int64Value: '1' } }],
96
+ metric: {
97
+ type: 'custom.googleapis.com/lacs'
98
+ }
99
+ }
100
+ ]);
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const logger = require('@emartech/json-logger')('aggregate-metric-logger');
4
+ const monitoring = require('@google-cloud/monitoring');
5
+
6
+ exports.publishTimeSeries = async timeSeries => {
7
+ try {
8
+ const client = getMonitoringClient();
9
+ await client.createTimeSeries({
10
+ name: client.projectPath(process.env.GCP_PROJECT_ID),
11
+ timeSeries
12
+ });
13
+ } catch (error) {
14
+ logger.warnFromError('pushing-metrics-failed', error, { do_not_alert: true });
15
+ }
16
+ };
17
+
18
+ let monitoringClientInstance = null;
19
+ const getMonitoringClient = () => {
20
+ if (!monitoringClientInstance) {
21
+ monitoringClientInstance = new monitoring.MetricServiceClient({
22
+ projectId: process.env.GCP_PROJECT_ID
23
+ });
24
+ }
25
+ return monitoringClientInstance;
26
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { MetricServiceClient } = require('@google-cloud/monitoring');
4
+ const { publishTimeSeries } = require('./gcp-time-series-publisher');
5
+ const { stubCreateTimeSeries } = require('../../test-helpers/test-helpers');
6
+
7
+ jest.mock('@google-cloud/monitoring');
8
+
9
+ describe('gcpMetricPublisher', () => {
10
+ describe('publishTimeSeries', () => {
11
+ it('should push a metrics to gcp in the correct format', async () => {
12
+ stubCreateTimeSeries();
13
+ process.env.HOSTNAME = 'test-host';
14
+ process.env.GCP_PROJECT_ID = 'test-project';
15
+ const timeSeries = [1, 2];
16
+
17
+ await publishTimeSeries(timeSeries);
18
+
19
+ expect(MetricServiceClient.prototype.createTimeSeries).toHaveBeenCalledWith({
20
+ name: 'projects/test-project',
21
+ timeSeries
22
+ });
23
+ });
24
+ });
25
+ });
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const metricLoggerFactory = require('./metric-logger');
4
- const gcpMetricCollector = require('./gcp-metric-collector');
3
+ const metricLoggerFactory = require('./laas-metric-logger/laas-metric-logger');
4
+ const gcpMetricCollector = require('./gcp-metric-collector/gcp-metric-collector');
5
5
 
6
6
  const enabled = process.env.METRIC_LOGGER_ENABLED === 'true';
7
7
 
@@ -24,5 +24,6 @@ exposedFactoryInstanceHybrid.stop = exposedInstance.stop;
24
24
  exposedFactoryInstanceHybrid.cancel = exposedInstance.cancel;
25
25
 
26
26
  exposedFactoryInstanceHybrid.count = gcpMetricCollector.count;
27
+ exposedFactoryInstanceHybrid.measureGcp = gcpMetricCollector.measure;
27
28
 
28
29
  module.exports = exposedFactoryInstanceHybrid;
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const metricLoggerFactory = require('./metric-logger');
4
3
  const Logger = require('@emartech/json-logger').Logger;
5
4
  const fakeTimers = require('@sinonjs/fake-timers');
5
+ const metricLoggerFactory = require('./laas-metric-logger');
6
6
 
7
7
  let metricLogger;
8
8
  let clock;
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ exports.createCounter = () => {
4
+ let count = 0;
5
+ const startTime = Date.now();
6
+
7
+ return {
8
+ getState: () => ({
9
+ startTime,
10
+ endTime: Date.now(),
11
+ metricKind: 'CUMULATIVE',
12
+ valueType: 'int64Value',
13
+ value: { int64Value: `${count}` }
14
+ }),
15
+ add: value => (count += value),
16
+ advanceTimer: () => {}
17
+ };
18
+ };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const { createCounter } = require('./counter');
4
+
5
+ describe('count', () => {
6
+ it('should return an empty counter if no values were added', () => {
7
+ jest.useFakeTimers().setSystemTime(new Date(880));
8
+ const counter = createCounter();
9
+
10
+ expect(counter.getState()).toEqual({
11
+ startTime: 880,
12
+ endTime: 880,
13
+ metricKind: 'CUMULATIVE',
14
+ valueType: 'int64Value',
15
+ value: { int64Value: '0' }
16
+ });
17
+ });
18
+
19
+ it('should increment counter when a value is added', () => {
20
+ const counter = createCounter();
21
+
22
+ counter.add(2);
23
+ counter.add(5);
24
+
25
+ expect(counter.getState()).toMatchObject({ value: { int64Value: '7' } });
26
+ });
27
+
28
+ it('should not reset the current count on advanceTimer call', () => {
29
+ jest.useFakeTimers().setSystemTime(new Date(880));
30
+ const counter = createCounter();
31
+
32
+ jest.advanceTimersByTime(20);
33
+ counter.add(2);
34
+ counter.advanceTimer();
35
+ counter.add(1);
36
+
37
+ expect(counter.getState()).toMatchObject({
38
+ value: { int64Value: '3' },
39
+ startTime: 880,
40
+ endTime: 900
41
+ });
42
+ });
43
+
44
+ it('should return current time as endTime', () => {
45
+ jest.useFakeTimers().setSystemTime(new Date(880));
46
+ const counter = createCounter();
47
+
48
+ jest.advanceTimersByTime(20);
49
+
50
+ expect(counter.getState()).toMatchObject({ endTime: 900 });
51
+ });
52
+ });
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ // exponential bucket rules: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TypedValue#exponential
4
+
5
+ exports.createDistribution = ({ scale = 1, growthFactor = 2, numFiniteBuckets = 20 } = {}) => {
6
+ const bucketUpperBounds = Array(numFiniteBuckets + 2)
7
+ .fill(null)
8
+ .map((value, index, array) =>
9
+ index === array.length - 1 ? Infinity : scale * growthFactor ** index
10
+ );
11
+ const createInitialState = () => ({
12
+ count: 0,
13
+ mean: 0,
14
+ sumOfSquaredDeviation: 0,
15
+ bucketCounts: bucketUpperBounds.map(() => 0)
16
+ });
17
+ let state = createInitialState();
18
+
19
+ return {
20
+ add: value => {
21
+ state.count += 1;
22
+ const previousMean = state.mean;
23
+ state.mean = state.mean + (value - state.mean) / state.count;
24
+ // calculation from https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
25
+ state.sumOfSquaredDeviation =
26
+ state.sumOfSquaredDeviation + (value - previousMean) * (value - state.mean);
27
+
28
+ const index = bucketUpperBounds.findIndex(bound => value < bound);
29
+ state.bucketCounts[index] += 1;
30
+ },
31
+ getState: () => ({
32
+ endTime: Date.now(),
33
+ startTime: Date.now(),
34
+ metricKind: 'GAUGE',
35
+ valueType: 'DISTRIBUTION',
36
+ value: {
37
+ distributionValue: {
38
+ ...state,
39
+ bucketOptions: {
40
+ exponentialBuckets: { numFiniteBuckets, growthFactor, scale }
41
+ }
42
+ }
43
+ }
44
+ }),
45
+ advanceTimer: () => {
46
+ state = createInitialState();
47
+ }
48
+ };
49
+ };
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ const { sum } = require('lodash');
4
+ const { createDistribution } = require('./distribution');
5
+
6
+ describe('distribution', () => {
7
+ it('should return an empty distribution if no values were added', () => {
8
+ jest.useFakeTimers().setSystemTime(new Date(15000));
9
+ const distribution = createDistribution({ scale: 1, growFactor: 2, numFiniteBuckets: 2 });
10
+
11
+ expect(distribution.getState()).toEqual({
12
+ startTime: 15000,
13
+ endTime: 15000,
14
+ metricKind: 'GAUGE',
15
+ valueType: 'DISTRIBUTION',
16
+ value: {
17
+ distributionValue: {
18
+ count: 0,
19
+ mean: 0,
20
+ sumOfSquaredDeviation: 0,
21
+ bucketOptions: { exponentialBuckets: { numFiniteBuckets: 2, growthFactor: 2, scale: 1 } },
22
+ bucketCounts: [0, 0, 0, 0]
23
+ }
24
+ }
25
+ });
26
+ });
27
+
28
+ it('should calculate the counter of all added values', () => {
29
+ const distribution = createDistribution();
30
+
31
+ distribution.add(2);
32
+ distribution.add(3);
33
+ distribution.add(16);
34
+
35
+ expect(distribution.getState().value.distributionValue).toMatchObject({ count: 3 });
36
+ });
37
+
38
+ it('should calculate the mean of all added values', () => {
39
+ const distribution = createDistribution();
40
+
41
+ distribution.add(2);
42
+ distribution.add(3);
43
+ distribution.add(16);
44
+
45
+ expect(distribution.getState().value.distributionValue).toMatchObject({ mean: 7 });
46
+ });
47
+
48
+ it('should calculate the sum of square deviation', () => {
49
+ const distribution = createDistribution({ numFiniteBuckets: 2 });
50
+
51
+ const values = [2, 3, 16];
52
+ const mean = sum(values) / values.length;
53
+ const sumOfSquaredDeviation = sum(values.map(value => (value - mean) ** 2));
54
+
55
+ values.forEach(value => distribution.add(value));
56
+
57
+ expect(distribution.getState().value.distributionValue).toMatchObject({
58
+ sumOfSquaredDeviation
59
+ });
60
+ });
61
+
62
+ describe('calculating bucketCounts', () => {
63
+ it('should add a negative number to the underflow bucket', () => {
64
+ const distribution = createDistribution({ numFiniteBuckets: 2 });
65
+
66
+ distribution.add(-12);
67
+ distribution.add(-12);
68
+
69
+ expect(distribution.getState().value.distributionValue).toMatchObject({
70
+ bucketCounts: [2, 0, 0, 0]
71
+ });
72
+ });
73
+
74
+ it('should increment the correct bucket when a value is added', () => {
75
+ const distribution = createDistribution({
76
+ scale: 1,
77
+ growFactor: 2,
78
+ numFiniteBuckets: 2
79
+ });
80
+
81
+ distribution.add(0.15);
82
+ distribution.add(1.2);
83
+ distribution.add(1.45);
84
+ distribution.add(3);
85
+
86
+ expect(distribution.getState().value.distributionValue).toMatchObject({
87
+ bucketCounts: [1, 2, 1, 0]
88
+ });
89
+ });
90
+
91
+ it('should correctly make the upper bound exclusive, the lower bound inclusive', () => {
92
+ const distribution = createDistribution({
93
+ scale: 1,
94
+ growFactor: 2,
95
+ numFiniteBuckets: 2
96
+ });
97
+
98
+ distribution.add(4);
99
+
100
+ expect(distribution.getState().value.distributionValue).toMatchObject({
101
+ bucketCounts: [0, 0, 0, 1]
102
+ });
103
+ });
104
+
105
+ it('should add overly huge value to the overflow bucket', () => {
106
+ const distribution = createDistribution({
107
+ scale: 1,
108
+ growFactor: 2,
109
+ numFiniteBuckets: 2
110
+ });
111
+
112
+ distribution.add(168);
113
+
114
+ expect(distribution.getState().value.distributionValue).toMatchObject({
115
+ bucketCounts: [0, 0, 0, 1]
116
+ });
117
+ });
118
+
119
+ it('should reset internal stats after advanceTimer call and start from initial values for next round', () => {
120
+ const distribution = createDistribution({
121
+ scale: 1,
122
+ growFactor: 2,
123
+ numFiniteBuckets: 2
124
+ });
125
+
126
+ distribution.add(1);
127
+ distribution.advanceTimer();
128
+
129
+ expect(distribution.getState().value.distributionValue).toMatchObject({
130
+ count: 0,
131
+ mean: 0,
132
+ sumOfSquaredDeviation: 0,
133
+ bucketCounts: [0, 0, 0, 0]
134
+ });
135
+ });
136
+ });
137
+ });
@@ -1,7 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  const { MetricServiceClient } = require('@google-cloud/monitoring');
4
- const { omit } = require('lodash');
4
+ const { omit, pick } = require('lodash');
5
+
6
+ exports.stubCreateTimeSeriesWithCallbackDuringPublish = (createMetric) => {
7
+ jest.spyOn(MetricServiceClient.prototype, 'createTimeSeries').mockImplementation(
8
+ () =>
9
+ new Promise(resolve => {
10
+ createMetric();
11
+ resolve();
12
+ })
13
+ );
14
+ };
5
15
 
6
16
  exports.stubCreateTimeSeries = () => {
7
17
  jest.spyOn(MetricServiceClient.prototype, 'createTimeSeries').mockResolvedValue();
@@ -14,9 +24,12 @@ exports.getPublishedMetrics = () => {
14
24
 
15
25
  return mock.calls.map(call =>
16
26
  call[0].timeSeries.map(series => ({
27
+ ...pick(series, ['metricKind', 'valueType']),
17
28
  name: series.metric.type.replace('custom.googleapis.com/', ''),
18
29
  labels: omit(series.metric.labels, 'hostname'),
19
- count: Number(series.points[0].value.int64Value)
30
+ startTime: series.points[0].interval.startTime.seconds * 1000,
31
+ endTime: series.points[0].interval.endTime.seconds * 1000,
32
+ value: series.points[0].value
20
33
  }))
21
34
  );
22
35
  };
@@ -1,41 +0,0 @@
1
- 'use strict';
2
-
3
- const stringify = require('json-stable-stringify');
4
- const { publishCountMetrics } = require('./gcp-metric-publisher');
5
-
6
- let metrics = {};
7
- let initialized = false;
8
- let startTime = null;
9
- let timeoutId = null;
10
-
11
- exports.count = (metricName, labels = {}, value = 1) => {
12
- if (process.env.METRIC_LOGGER_ENABLED !== 'true') return;
13
-
14
- if (!initialized) {
15
- initialized = true;
16
- startTime = Date.now();
17
- setupNextFlush();
18
- }
19
- const key = `${metricName}_${stringify(labels)}`;
20
- if (!metrics[key]) {
21
- metrics[key] = { name: metricName, labels, count: value };
22
- } else {
23
- metrics[key].count += value;
24
- }
25
- };
26
-
27
- exports.reset = () => {
28
- metrics = {};
29
- initialized = false;
30
- startTime = null;
31
- clearTimeout(timeoutId);
32
- };
33
-
34
- const setupNextFlush = () => {
35
- timeoutId = setTimeout(flush, 60 * 1000);
36
- };
37
-
38
- const flush = async () => {
39
- await publishCountMetrics(metrics, startTime);
40
- setupNextFlush();
41
- };
@@ -1,89 +0,0 @@
1
- 'use strict';
2
-
3
- const gcpMetricCollector = require('./gcp-metric-collector');
4
- const {
5
- stubCreateTimeSeries,
6
- getPublishedMetrics,
7
- flushPromises
8
- } = require('../test-helpers/test-helpers');
9
-
10
- beforeEach(() => {
11
- jest.useFakeTimers();
12
- gcpMetricCollector.reset();
13
- });
14
- afterEach(() => {
15
- jest.useRealTimers();
16
- gcpMetricCollector.reset();
17
- });
18
-
19
- describe('gcpMetricCollector', () => {
20
- describe('count', () => {
21
- it('should send metric for a single measurement in next flush period', () => {
22
- stubCreateTimeSeries();
23
-
24
- gcpMetricCollector.count('jolo', { 'kacsa': 'A' }, 88);
25
-
26
- expect(getPublishedMetrics()).toEqual([]);
27
-
28
- jest.advanceTimersByTime(60 * 1000 + 1);
29
-
30
- expect(getPublishedMetrics()).toEqual([
31
- [{ name: 'jolo', labels: { kacsa: 'A' }, count: 88 }]
32
- ]);
33
- });
34
-
35
- it('should sum up counts that happened and publish the sum, send a metric for each label combo', () => {
36
- stubCreateTimeSeries();
37
-
38
- gcpMetricCollector.count('jolo', { a: 1 });
39
- gcpMetricCollector.count('jolo');
40
- gcpMetricCollector.count('yolo');
41
- gcpMetricCollector.count('yolo');
42
- gcpMetricCollector.count('jolo');
43
-
44
- expect(getPublishedMetrics()).toEqual([]);
45
-
46
- jest.advanceTimersByTime(60 * 1000 + 1);
47
-
48
- expect(getPublishedMetrics()).toEqual([
49
- [
50
- { name: 'jolo', labels: { a: 1 }, count: 1 },
51
- { name: 'jolo', labels: {}, count: 2 },
52
- { name: 'yolo', labels: {}, count: 2 }
53
- ]
54
- ]);
55
- });
56
-
57
- it('should not reset counts after sending metrics, but keep increasing them', async () => {
58
- stubCreateTimeSeries();
59
-
60
- gcpMetricCollector.count('jolo');
61
- gcpMetricCollector.count('jolo');
62
-
63
- jest.advanceTimersByTime(60 * 1000 + 1);
64
- await flushPromises();
65
- expect(getPublishedMetrics()).toEqual([
66
- [{ name: 'jolo', labels: {}, count: 2 }]
67
- ]);
68
-
69
- gcpMetricCollector.count('jolo');
70
- jest.advanceTimersByTime(60 * 1000 + 1);
71
- await flushPromises();
72
-
73
- expect(getPublishedMetrics()).toEqual([
74
- [{ name: 'jolo', labels: {}, count: 2 }],
75
- [{ name: 'jolo', labels: {}, count: 3 }]
76
- ]);
77
- });
78
-
79
- it('should not publish metrics if metric logger is not enabled', async () => {
80
- process.env.METRIC_LOGGER_ENABLED = 'false';
81
- stubCreateTimeSeries();
82
-
83
- gcpMetricCollector.count('jolo', { 'kacsa': 'A' }, 88);
84
- jest.advanceTimersByTime(60 * 1000 + 1);
85
-
86
- expect(getPublishedMetrics()).toEqual([]);
87
- });
88
- });
89
- });
@@ -1,48 +0,0 @@
1
- 'use strict';
2
-
3
- const logger = require('@emartech/json-logger')('aggregate-metric-logger');
4
- const monitoring = require('@google-cloud/monitoring');
5
-
6
- exports.publishCountMetrics = async (metrics, startTime) => {
7
- const timeSeries = Object.values(metrics).map(metric => ({
8
- metric: {
9
- type: `custom.googleapis.com/${metric.name}`,
10
- labels: { ...metric.labels, hostname: process.env.HOSTNAME }
11
- },
12
- resource: {
13
- type: 'global',
14
- labels: { project_id: process.env.GCP_PROJECT_ID }
15
- },
16
- points: [
17
- {
18
- interval: {
19
- startTime: { seconds: startTime / 1000 },
20
- endTime: { seconds: Date.now() / 1000 }
21
- },
22
- value: { int64Value: `${metric.count}` }
23
- }
24
- ],
25
- metricKind: 'CUMULATIVE',
26
- valueType: 'int64Value'
27
- }));
28
- try {
29
- const client = getMonitoringClient();
30
- await client.createTimeSeries({
31
- name: client.projectPath(process.env.GCP_PROJECT_ID),
32
- timeSeries
33
- });
34
- } catch (error) {
35
- logger.warnFromError('pushing-metrics-failed', error, { do_not_alert: true });
36
- }
37
- };
38
-
39
- let monitoringClientInstance = null;
40
- const getMonitoringClient = () => {
41
- if (!monitoringClientInstance) {
42
- monitoringClientInstance = new monitoring.MetricServiceClient({
43
- projectId: process.env.GCP_PROJECT_ID,
44
- credentials: JSON.parse(process.env.GCP_CREDENTIALS || '{}')
45
- });
46
- }
47
- return monitoringClientInstance;
48
- };
@@ -1,68 +0,0 @@
1
- 'use strict';
2
-
3
- const { MetricServiceClient } = require('@google-cloud/monitoring');
4
- const { publishCountMetrics } = require('./gcp-metric-publisher');
5
- const { stubCreateTimeSeries, getPublishedMetrics } = require('../test-helpers/test-helpers');
6
-
7
- jest.mock('@google-cloud/monitoring');
8
-
9
- describe('gcpMetricPublisher', () => {
10
- describe('publishCountMetrics', () => {
11
- it('should push a metrics to gcp in the correct format', async () => {
12
- stubCreateTimeSeries();
13
- jest.useFakeTimers().setSystemTime(new Date(80000));
14
- process.env.HOSTNAME = 'test-host';
15
- process.env.GCP_PROJECT_ID = 'test-project';
16
- const metrics = {
17
- key1: { name: 'kacs', labels: { a: 1 }, count: 14 }
18
- };
19
- const startTime = 15000;
20
-
21
- await publishCountMetrics(metrics, startTime);
22
-
23
- expect(MetricServiceClient.prototype.createTimeSeries).toHaveBeenCalledWith({
24
- name: 'projects/test-project',
25
- timeSeries: [
26
- {
27
- metric: {
28
- type: 'custom.googleapis.com/kacs',
29
- labels: { a: 1, hostname: 'test-host' }
30
- },
31
- resource: { type: 'global', labels: { project_id: 'test-project' } },
32
- points: [
33
- {
34
- interval: {
35
- startTime: { seconds: 15 },
36
- endTime: { seconds: 80 }
37
- },
38
- value: { int64Value: '14' }
39
- }
40
- ],
41
- metricKind: 'CUMULATIVE',
42
- valueType: 'int64Value'
43
- }
44
- ]
45
- });
46
- });
47
-
48
- it('should publish multile metrics with the same api call', async () => {
49
- stubCreateTimeSeries();
50
- const metrics = {
51
- key1: { name: 'kacs', labels: { a: 1 }, count: 14 },
52
- key2: { name: 'kacs', labels: { a: 2 }, count: 88 },
53
- key3: { name: 'lacs', labels: {}, count: 1 }
54
- };
55
- const startTime = 15000;
56
-
57
- await publishCountMetrics(metrics, startTime);
58
-
59
- expect(getPublishedMetrics()).toEqual([
60
- [
61
- { name: 'kacs', labels: { a: 1 }, count: 14 },
62
- { name: 'kacs', labels: { a: 2 }, count: 88 },
63
- { name: 'lacs', labels: {}, count: 1 }
64
- ]
65
- ]);
66
- });
67
- });
68
- });