@emartech/aggregate-metric-logger 5.0.2 → 5.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/README.md CHANGED
@@ -18,6 +18,8 @@ Set `METRIC_LOGGER_ENABLED` just us for LaaS based metrics.
18
18
  Set `MONITORING_PROJECT_ID` and `GCP_CREDENTIALS` env variables. `HOSTNAME` env variable also has to be
19
19
  set, but for a pod running on kubernetes this should already be set.
20
20
 
21
+ Add `Monitoring Metrics Writer` Role to the service account in the GCP project.
22
+
21
23
 
22
24
  ## Usage
23
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emartech/aggregate-metric-logger",
3
- "version": "5.0.2",
3
+ "version": "5.2.0",
4
4
  "description": "aggregate metrics in memory for logging",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -30,19 +30,19 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@emartech/json-logger": "^8.0.3",
33
- "@google-cloud/monitoring": "^3.0.2",
34
- "date-fns": "^2.3.0",
33
+ "@google-cloud/monitoring": "^4.1.0",
34
+ "date-fns": "^3.6.0",
35
35
  "json-stable-stringify": "^1.1.1",
36
36
  "lodash": "^4.17.15",
37
37
  "ms": "^2.1.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@emartech/rti-server-tools": "^6.13.8",
40
+ "@emartech/rti-server-tools": "^6.17.0",
41
41
  "@sinonjs/fake-timers": "^11.2.2",
42
42
  "@types/jest": "^29.5.12",
43
43
  "dotenv-haphap": "^3.1.2",
44
- "eslint": "^9.4.0",
45
- "globals": "15.4.0",
44
+ "eslint": "^9.7.0",
45
+ "globals": "15.8.0",
46
46
  "jest": "^29.2.2",
47
47
  "semantic-release": "^24.0.0"
48
48
  }
@@ -1,71 +1,88 @@
1
1
  'use strict';
2
2
 
3
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');
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
- let metrics = {};
11
- let initialized = false;
12
- let timeoutId = null;
10
+ class GcpMetricCollector {
11
+ metrics = {};
12
+ initialized = false;
13
+ timeoutId = null;
14
+ projectId = null;
13
15
 
14
- exports.count = (metricName, labels = {}, value = 1) => {
15
- if (!isNumber(value)) {
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
- if (!isPlainObject(labels)) {
20
- throw new Error('labels is not a plain object');
21
- }
20
+ count(metricName, labels = {}, value = 1) {
21
+ if (!isNumber(value)) {
22
+ throw new Error(`${ value } is not a number`);
23
+ }
22
24
 
23
- addToMetric(metricName, labels, 'counter', value);
24
- };
25
+ if (!isPlainObject(labels)) {
26
+ throw new Error('labels is not a plain object');
27
+ }
25
28
 
26
- exports.measure = (metricName, labels = {}, value = 1) =>
27
- addToMetric(metricName, labels, 'distribution', value);
29
+ this.addToMetric(metricName, labels, 'counter', value);
30
+ }
28
31
 
29
- exports.start = (metricName, labels = {}) => ({ metricName, labels, start: Date.now() });
32
+ measure(metricName, labels = {}, value = 1) {
33
+ return this.addToMetric(metricName, labels, 'distribution', value);
34
+ }
35
+
36
+ start(metricName, labels = {}) {
37
+ return { metricName, labels, start: Date.now() };
38
+ }
30
39
 
31
- exports.stop = measurement =>
32
- addToMetric(
33
- measurement.metricName,
34
- measurement.labels,
35
- 'distribution',
36
- Date.now() - measurement.start
37
- );
40
+ stop(measurement) {
41
+ this.addToMetric(
42
+ measurement.metricName,
43
+ measurement.labels,
44
+ 'distribution',
45
+ Date.now() - measurement.start
46
+ );
47
+ }
38
48
 
39
- const addToMetric = (metricName, labels = {}, type, value) => {
40
- if (process.env.METRIC_LOGGER_ENABLED !== 'true') return;
49
+ addToMetric(metricName, labels = {}, type, value) {
50
+ if (process.env.METRIC_LOGGER_ENABLED !== 'true') return;
41
51
 
42
- if (!initialized) {
43
- initialized = true;
44
- setupNextFlush();
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
- const key = `${metricName}_${stringify(labels)}`;
47
- if (!metrics[key]) {
48
- const aggregator = type === 'counter' ? createCounter() : createDistribution();
49
- metrics[key] = { name: metricName, labels, aggregator };
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
- exports.reset = () => {
55
- metrics = {};
56
- initialized = false;
57
- clearTimeout(timeoutId);
58
- };
70
+ setupNextFlush() {
71
+ this.timeoutId = setTimeout(this.flush.bind(this), 60 * 1000);
72
+ }
59
73
 
60
- const setupNextFlush = () => {
61
- timeoutId = setTimeout(flush, 60 * 1000);
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
- const flush = async () => {
65
- const timeSeries = buildTimeSeries(metrics);
66
- for (const [, metric] of Object.entries(metrics)) {
67
- metric.aggregator.advanceTimer();
83
+ createGcpMetricsCollector(projectId) {
84
+ return new GcpMetricCollector(projectId);
68
85
  }
69
- await publishTimeSeries(timeSeries);
70
- setupNextFlush();
71
- };
86
+ }
87
+
88
+ module.exports = new GcpMetricCollector();
@@ -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
- exports.buildTimeSeries = metrics =>
4
- Object.values(metrics).map(metric => {
5
- const state = metric.aggregator.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.MONITORING_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
- });
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
- exports.publishTimeSeries = async timeSeries => {
12
- try {
13
- const client = getMonitoringClient();
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
- const createTimeSeries = async (client, timeSeries) => {
23
- const name = client.projectPath(process.env.MONITORING_PROJECT_ID);
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
- let monitoringClientInstance = null;
33
- const getMonitoringClient = () => {
34
- if (!monitoringClientInstance) {
35
- monitoringClientInstance = new monitoring.MetricServiceClient({
36
- projectId: process.env.MONITORING_PROJECT_ID
37
- });
38
- }
39
- return monitoringClientInstance;
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,