@emartech/aggregate-metric-logger 2.0.5 → 2.1.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
@@ -9,6 +9,16 @@ start collecting and logging measurements.
9
9
  To change the default `aggregate-metric-logger` namespace used for logs, set
10
10
  `METRIC_LOGGER_NAMESPACE` to a custom value.
11
11
 
12
+ ## Setup to send metrics to GCP monitoring
13
+
14
+ (currently only the `metricLogger.count` method sends metrics to GCP)
15
+
16
+ Set `METRIC_LOGGER_ENABLED` just us for LaaS based metrics.
17
+
18
+ Set `GCP_PROJECT_ID` and `GCP_CREDENTIALS` env variables. `HOSTNAME` env variable also has to be
19
+ set, but for a pod running on kubernetes this should already be set.
20
+
21
+
12
22
  ## Usage
13
23
 
14
24
  The package exposes similar logging methods as the `@emartech/json-logger` package:
package/env/.env ADDED
@@ -0,0 +1,3 @@
1
+ HOSTNAME=local-machine
2
+ GCP_PROJECT_ID=dummy-project
3
+ METRIC_LOGGER_ENABLED=true
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ testEnvironment: 'node',
5
+ setupFilesAfterEnv: ['./test-helpers/test-setup.js'],
6
+ restoreMocks: true,
7
+ transform: {}
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emartech/aggregate-metric-logger",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "aggregate metrics in memory for logging",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@emartech/json-logger": "^3.1.0",
33
+ "@google-cloud/monitoring": "^3.0.2",
33
34
  "date-fns": "^2.3.0",
34
35
  "json-stable-stringify": "^1.0.1",
35
36
  "lodash": "^4.17.15"
@@ -37,10 +38,11 @@
37
38
  "devDependencies": {
38
39
  "@sinonjs/fake-timers": "9.1.1",
39
40
  "@types/jest": "27.4.1",
41
+ "dotenv-haphap": "^3.1.2",
40
42
  "eslint": "^8.12.0",
41
43
  "eslint-config-emarsys": "^5.1.0",
42
44
  "eslint-plugin-jest": "^26.1.3",
43
- "jest": "27.5.1",
45
+ "jest": "^29.2.2",
44
46
  "semantic-release": "^19.0.2"
45
47
  }
46
48
  }
@@ -0,0 +1,41 @@
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
+ };
@@ -0,0 +1,89 @@
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
+ });
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const monitoring = require('@google-cloud/monitoring');
4
+ const client = new monitoring.MetricServiceClient();
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
+ await client.createTimeSeries({
29
+ name: client.projectPath(process.env.GCP_PROJECT_ID),
30
+ timeSeries
31
+ });
32
+ };
@@ -0,0 +1,68 @@
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
+ });
package/src/index.js CHANGED
@@ -1,21 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  const metricLoggerFactory = require('./metric-logger');
4
+ const gcpMetricCollector = require('./gcp-metric-collector');
4
5
 
5
6
  const enabled = process.env.METRIC_LOGGER_ENABLED === 'true';
6
- const inProgressMeasurementWarningLimit =
7
- parseInt(process.env.METRIC_LOGGER_IN_PROGRESS_MEASUREMENT_WARNING_LIMIT, 10) || 10000;
8
7
 
9
8
  const exposedInstance = metricLoggerFactory({
10
9
  enabled,
11
- namespace: process.env.METRIC_LOGGER_NAMESPACE || 'aggregate-metric-logger',
12
- inProgressMeasurementWarningLimit
10
+ namespace: process.env.METRIC_LOGGER_NAMESPACE || 'aggregate-metric-logger'
13
11
  });
14
12
  const exposedFactoryInstanceHybrid = namespace => metricLoggerFactory({ enabled, namespace });
15
13
 
16
14
  exposedFactoryInstanceHybrid.setThresholds = exposedInstance.setThresholds;
17
15
  exposedFactoryInstanceHybrid.measure = exposedInstance.measure;
18
- exposedFactoryInstanceHybrid.count = exposedInstance.count;
19
16
  exposedFactoryInstanceHybrid.trace = exposedInstance.trace;
20
17
  exposedFactoryInstanceHybrid.debug = exposedInstance.debug;
21
18
  exposedFactoryInstanceHybrid.info = exposedInstance.info;
@@ -26,4 +23,6 @@ exposedFactoryInstanceHybrid.start = exposedInstance.start;
26
23
  exposedFactoryInstanceHybrid.stop = exposedInstance.stop;
27
24
  exposedFactoryInstanceHybrid.cancel = exposedInstance.cancel;
28
25
 
26
+ exposedFactoryInstanceHybrid.count = gcpMetricCollector.count;
27
+
29
28
  module.exports = exposedFactoryInstanceHybrid;
@@ -4,11 +4,10 @@ const metricLoggerFactory = require('./metric-logger');
4
4
  const Logger = require('@emartech/json-logger').Logger;
5
5
  const fakeTimers = require('@sinonjs/fake-timers');
6
6
 
7
- jest.spyOn(Logger.prototype, 'info');
8
7
  let metricLogger;
9
8
  let clock;
10
9
  beforeEach(() => {
11
- jest.resetAllMocks();
10
+ jest.spyOn(Logger.prototype, 'info');
12
11
  metricLogger = metricLoggerFactory();
13
12
  clock = fakeTimers.install();
14
13
  });
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { MetricServiceClient } = require('@google-cloud/monitoring');
4
+ const { omit } = require('lodash');
5
+
6
+ exports.stubCreateTimeSeries = () => {
7
+ jest.spyOn(MetricServiceClient.prototype, 'createTimeSeries').mockResolvedValue();
8
+ };
9
+
10
+ exports.getPublishedMetrics = () => {
11
+ const { mock } = MetricServiceClient.prototype.createTimeSeries;
12
+
13
+ if (mock.calls.length === 0) return [];
14
+
15
+ return mock.calls.map(call =>
16
+ call[0].timeSeries.map(series => ({
17
+ name: series.metric.type.replace('custom.googleapis.com/', ''),
18
+ labels: omit(series.metric.labels, 'hostname'),
19
+ count: Number(series.points[0].value.int64Value)
20
+ }))
21
+ );
22
+ };
23
+
24
+ exports.flushPromises = async () => new Promise(jest.requireActual('timers').setImmediate);
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ require('dotenv-haphap').config('env/.env');
4
+
5
+ const env = process.env;
6
+
7
+ beforeEach(() => {
8
+ process.env = { ...env };
9
+ });
10
+
11
+ afterEach(() => {
12
+ process.env = env;
13
+ });