@pager/minion-army 2.0.0 → 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
@@ -1,7 +1,7 @@
1
1
  module.exports = {
2
2
  extends: '@hapi/eslint-config-hapi',
3
3
  parserOptions: {
4
- ecmaVersion: 9
4
+ ecmaVersion: 12
5
5
  },
6
6
  rules: {
7
7
  'no-console': 2
@@ -9,13 +9,15 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - name: Checkout
12
- uses: actions/checkout@v1
12
+ uses: actions/checkout@v2
13
+ with:
14
+ persist-credentials: false
13
15
  - name: Setup Node.js
14
- uses: actions/setup-node@v1
16
+ uses: actions/setup-node@v2
15
17
  with:
16
- node-version: 12
18
+ node-version: 18
17
19
  - name: Cache
18
- uses: actions/cache@v1
20
+ uses: actions/cache@v2
19
21
  with:
20
22
  path: ~/.npm
21
23
  key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }}
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.1.0](https://github.com/pagerinc/minion-army/compare/v2.0.0...v2.1.0) (2023-05-08)
2
+
3
+
4
+ ### Features
5
+
6
+ * add requeue to the schema ([#453](https://github.com/pagerinc/minion-army/issues/453)) ([74a5c41](https://github.com/pagerinc/minion-army/commit/74a5c4127aee8513500edfe04760bf03ffee057f))
7
+
1
8
  # [2.0.0](https://github.com/pagerinc/minion-army/compare/v1.3.0...v2.0.0) (2020-08-19)
2
9
 
3
10
 
@@ -0,0 +1,39 @@
1
+ steps:
2
+ - id: npm-install
3
+ name: node:18
4
+ secretEnv: ['NPM_TOKEN']
5
+ entrypoint: npm
6
+ args: ['i', '--quiet', '--package-lock-only']
7
+
8
+ - id: npm-ci
9
+ name: node:18
10
+ secretEnv: ['NPM_TOKEN']
11
+ entrypoint: npm
12
+ args: ['ci', '--quiet']
13
+
14
+ - id: test-unit
15
+ name: node:18
16
+ secretEnv: ['NPM_TOKEN']
17
+ entrypoint: npm
18
+ args: ['test']
19
+
20
+ - id: npm-publish
21
+ name: 'gcr.io/$PROJECT_ID/cloudbuilders/npm:6.13.4'
22
+ secretEnv: ['NPM_TOKEN']
23
+ env:
24
+ - 'TAG_NAME=$TAG_NAME'
25
+ - '_PR_NUMBER=$_PR_NUMBER'
26
+
27
+ timeout: 10m
28
+
29
+ logsBucket: 'gs://$PROJECT_ID-primary-cloudbuild-logs'
30
+
31
+ tags:
32
+ - 'backend'
33
+ - 'npm'
34
+ - 'nodejs'
35
+
36
+ secrets:
37
+ - kmsKeyName: projects/production-197117/locations/global/keyRings/gcb/cryptoKeys/main
38
+ secretEnv:
39
+ NPM_TOKEN: 'CiUA/4lqmXwRPPaGHe+X7TS7mwqARNCw5QFq7yfq7ESHaJrf+tzeElEADvOwrLQvnxCLG2wy+H2vD+DWHMosEgIfzpKNBJAVHX1u4FSwIF5utaN6tMIrLuZB18HnK2SKpsXTPvB/+0Eoz1acnj6WO+slz+GUGUnxefU='
package/lib/index.js CHANGED
@@ -5,8 +5,9 @@ const { EventEmitter } = require('events');
5
5
  const Jackrabbit = require('@pager/jackrabbit');
6
6
  const Joi = require('joi');
7
7
  const Schema = require('./schema');
8
+ const { createLoggerContext } = require('./loggingUtils');
8
9
 
9
- const validatorFactory = (handler, schema) => (payload, metadata) => { // eslint-disable-line
10
+ const validatorFactory = (handler, schema) => (payload, ...rest) => { // eslint-disable-line
10
11
 
11
12
  const { error, value } = schema.validate(payload, { stripUnknown: true });
12
13
 
@@ -14,7 +15,7 @@ const validatorFactory = (handler, schema) => (payload, metadata) => { // eslint
14
15
  throw error;
15
16
  }
16
17
 
17
- return handler(value, metadata);
18
+ return handler(value, ...rest);
18
19
  };
19
20
 
20
21
  const createExchange = (connection, workerConfig, exchangeMap) => {
@@ -83,9 +84,15 @@ module.exports = (_manifest) => {
83
84
  ...manifest.defaults,
84
85
  ...worker.config
85
86
  };
87
+
88
+ let handlerWithLoggerContext = handlerWithValidation;
89
+ if (manifest.logger) {
90
+ handlerWithLoggerContext = createLoggerContext(manifest.logger, worker.config.name, handlerWithValidation);
91
+ }
92
+
86
93
  const exchange = createExchange(manifest.connection, workerConfig, exchangeMap);
87
94
 
88
- const minion = Minion(handlerWithValidation, { ...workerConfig, exchange });
95
+ const minion = Minion(handlerWithLoggerContext, { ...workerConfig, exchange });
89
96
 
90
97
  minion.on('ready', checkReady);
91
98
  minion.on('message', (m, meta) => eventEmitter.emit('message', worker.config.name, m, meta));
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const { v4: Uuid } = require('uuid');
4
+
5
+ const Lodash = require('lodash');
6
+
7
+ /**
8
+ * This is a wrapper around the handler that creates a pino style logger and
9
+ * adds it to a context object as a third argument of the handler
10
+ *
11
+ * @param {*} logger a Pino style logger
12
+ * @param {*} minionWorkerName the name of the minion army worker
13
+ * @param {*} handler the handler to be wrapped
14
+ * @returns
15
+ */
16
+ exports.createLoggerContext = (logger, minionWorkerName, handler) => {
17
+
18
+ return (value, metadata, context = {}) => {
19
+
20
+ let eventId;
21
+ try {
22
+ eventId = metadata?.properties?.headers?.eventId || Uuid();
23
+ const routingKey = metadata?.fields?.routingKey;
24
+
25
+ context.logger = (context.logger || logger).child({ eventId, routingKey, minionWorkerName });
26
+ }
27
+ catch (error) {
28
+ // eslint-disable-next-line no-console
29
+ console.log('There was an error while creating a child logger in %s for eventId(%s): %o', minionWorkerName, eventId, error);
30
+ }
31
+
32
+ return handler(value, metadata, context);
33
+ };
34
+ };
35
+
36
+ /**
37
+ * This utility crteates a wrapper that adds a field from the event into the logger.
38
+ *
39
+ * Usage:
40
+ *
41
+ * const Logger = require('@pager/logger');
42
+ * const Army = require('@pager/minion-army');
43
+ *
44
+ * const injectEncounterIdFromEvent = injectFieldFromEventAs(Logger, 'encounterId', 'triageId');
45
+ *
46
+ * const handler = (message, metadata, context) {
47
+ * context.logger.info('handling message');
48
+ * }
49
+ *
50
+ * const army = Army({
51
+ * workers: [
52
+ * {
53
+ * handler: injectEncounterIdFromEvent(handler),
54
+ * config: {
55
+ * name: `events.foo.encounter.state.updated`,
56
+ * key: '#.encounter.state.updated'
57
+ * },
58
+ * validate: Schemas.states
59
+ * },
60
+ * });
61
+ *
62
+ * @param {*} logger a Pino style logger
63
+ * @param {*} loggedFieldName what is the name of the field in the log line
64
+ * @param {*} eventFieldName what is the name of the field in the event payload
65
+ * @returns
66
+ */
67
+ exports.injectFieldFromEventAs = (logger, loggedFieldName, eventFieldName) => (handler) => { // eslint-disable-line @hapi/hapi/scope-start, @hapi/hapi/no-arrowception
68
+
69
+ return (value, metadata, context = {}) => {
70
+
71
+ context.logger = (context.logger || logger).child({ [loggedFieldName]: value[eventFieldName] });
72
+
73
+ return handler(value, metadata, context);
74
+ };
75
+ };
76
+
77
+ /**
78
+ * This utility function sets up default event handlers that log
79
+ *
80
+ * Usage:
81
+ *
82
+ * const Logger = require('@pager/logger');
83
+ * const Army = require('@pager/minion-army');
84
+ *
85
+ * const army = Army({ ... });
86
+ * addDefaultLoggingEventHandlers(army, 'UpdatedUserArmy', Logger);
87
+ *
88
+ * @param {*} army: the Army instance
89
+ * @param {*} armyName: a unique identifier for this instance of Army
90
+ * @param {*} parentLogger: a Pino style logger
91
+ */
92
+ exports.addDefaultLoggingEventHandlers = (parentLogger, army, armyName) => {
93
+
94
+ const logger = parentLogger.child({ armyName });
95
+
96
+ army.on('error', (error) => {
97
+
98
+ logger.error({ error }, 'Handler error in %s', armyName);
99
+ });
100
+
101
+ army.on('message', (queue, event, metadata) => {
102
+
103
+ const pickedMetadata = Lodash.pick(metadata, ['properties', 'fields']);
104
+
105
+ logger.info({ queue, event, metadata: pickedMetadata }, 'Got event in %s', armyName);
106
+ });
107
+
108
+ army.on('ready', (queue) => {
109
+
110
+ logger.info({ queue }, 'Ready to consume on %s by %s', queue.name, armyName);
111
+ });
112
+ };
package/lib/schema.js CHANGED
@@ -19,7 +19,8 @@ const config = Joi.object({
19
19
  rabbit,
20
20
  rabbitUrl: Joi.string(),
21
21
  prefetch: Joi.number(),
22
- queueMode: Joi.string()
22
+ queueMode: Joi.string(),
23
+ requeue: Joi.boolean()
23
24
  });
24
25
 
25
26
  const worker = {
@@ -35,5 +36,8 @@ module.exports = Joi.object({
35
36
  rabbitUrl: Joi.string()
36
37
  }),
37
38
  defaults: config,
38
- workers: Joi.array().items(worker)
39
+ workers: Joi.array().items(worker),
40
+ logger: Joi.object({
41
+ child: Joi.func().required()
42
+ }).unknown(true)
39
43
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pager/minion-army",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Microservice Framework for RabbitMQ Workers",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -25,17 +25,20 @@
25
25
  "homepage": "https://github.com/pagerinc/minion#readme",
26
26
  "dependencies": {
27
27
  "@pager/jackrabbit": "5.x",
28
- "@pager/minion": "3.x"
28
+ "@pager/minion": "3.x",
29
+ "lodash": "^4.17.21"
29
30
  },
30
31
  "devDependencies": {
32
+ "@faker-js/faker": "^8.0.1",
31
33
  "@hapi/eslint-config-hapi": "13.x",
32
34
  "@hapi/eslint-plugin-hapi": "4.x",
33
- "@pager/semantic-release-config": "1.x",
35
+ "@pager/semantic-release-config": "2.x",
34
36
  "ava": "3.x",
35
37
  "eslint": "7.x",
36
38
  "joi": "^17.2.1",
37
39
  "nyc": "15.x",
38
- "semantic-release": "17.x"
40
+ "semantic-release": "21.x",
41
+ "sinon": "^15.0.4"
39
42
  },
40
43
  "peerDependencies": {
41
44
  "joi": "^17.2.0"
package/test/index.js CHANGED
@@ -4,8 +4,41 @@ const { EventEmitter } = require('events');
4
4
  const Test = require('ava');
5
5
  const Joi = require('joi');
6
6
 
7
+ const Sinon = require('sinon');
8
+ const { faker } = require('@faker-js/faker');
9
+
7
10
  const Army = require('../lib/index');
8
11
 
12
+ const sandbox = Sinon.createSandbox();
13
+
14
+
15
+ Test.beforeEach((t) => {
16
+
17
+ t.context.logger = {
18
+ child: sandbox.stub(),
19
+ info: sandbox.stub()
20
+ };
21
+ t.context.rabbit = {
22
+ topic: () => ({
23
+ publish: sandbox.spy()
24
+ }),
25
+ direct: () => ({
26
+ publish: sandbox.spy()
27
+ })
28
+ };
29
+ t.context.metadata = {
30
+ properties: {
31
+ headers: {
32
+ eventId: faker.string.uuid()
33
+ }
34
+ },
35
+ fields: {
36
+ routingKey: faker.string.uuid()
37
+ }
38
+ };
39
+
40
+ });
41
+
9
42
  Test('Creates army from manifest and workers work', async (t) => {
10
43
 
11
44
  const manifest = {
@@ -399,3 +432,49 @@ Test('Army uses worker exchange overrides', (t) => {
399
432
  const army = Army(manifest);
400
433
  t.deepEqual(Object.keys(army.exchangeMap), ['direct.events.something.happened', 'topic.my-other-exchange']);
401
434
  });
435
+
436
+
437
+ Test('Army starts with logger context', async (t) => {
438
+
439
+ const { context: { rabbit, logger, metadata } } = t;
440
+
441
+ const minionWorkerName = faker.word.sample();
442
+
443
+ const handler = sandbox.spy();
444
+
445
+ const manifest = {
446
+ connection: {
447
+ rabbit
448
+ },
449
+ defaults: {
450
+ exchangeName: faker.word.sample()
451
+ },
452
+ workers: [
453
+ {
454
+ handler,
455
+ config: {
456
+ name: minionWorkerName,
457
+ key: faker.word.sample()
458
+ }
459
+ }
460
+ ],
461
+ logger
462
+ };
463
+
464
+ const army = Army(manifest);
465
+ t.truthy(army);
466
+
467
+ const event = {
468
+ data: faker.word.sample()
469
+ };
470
+
471
+ const childLogger = sandbox.spy();
472
+
473
+ t.context.logger.child.returns(childLogger);
474
+
475
+ await army.minions[minionWorkerName].handle(event, metadata);
476
+
477
+ Sinon.assert.calledOnceWithExactly(logger.child, { eventId: metadata.properties.headers.eventId, routingKey: metadata.fields.routingKey, minionWorkerName });
478
+
479
+ Sinon.assert.calledOnceWithExactly(handler, event, metadata, { logger: childLogger });
480
+ });
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ const Test = require('ava');
4
+
5
+ const Sinon = require('sinon');
6
+ const { faker } = require('@faker-js/faker');
7
+
8
+ const Army = require('../lib/index');
9
+ const { createLoggerContext, injectFieldFromEventAs, addDefaultLoggingEventHandlers } = require('../lib/loggingUtils');
10
+
11
+ const sandbox = Sinon.createSandbox();
12
+
13
+
14
+ Test.beforeEach((t) => {
15
+
16
+ t.context.logger = {
17
+ child: sandbox.stub()
18
+ };
19
+ t.context.metadata = {
20
+ properties: {
21
+ headers: {
22
+ eventId: faker.string.uuid()
23
+ }
24
+ },
25
+ fields: {
26
+ routingKey: faker.string.uuid()
27
+ }
28
+ };
29
+ t.context.event = sandbox.spy();
30
+ t.context.minionWorkerName = faker.word.sample();
31
+ t.context.handler = sandbox.spy();
32
+ t.context.rabbit = {
33
+ topic: () => ({
34
+ publish: sandbox.spy()
35
+ })
36
+ };
37
+ });
38
+
39
+ Test.afterEach((t) => {
40
+
41
+ sandbox.restore();
42
+ });
43
+
44
+ Test('createLoggerContext wraps a handler', (t) => {
45
+
46
+ const { context: { logger, event, metadata, minionWorkerName, handler } } = t;
47
+
48
+ const wrappedHandler = createLoggerContext(logger, minionWorkerName, handler);
49
+
50
+ const childLogger = sandbox.spy();
51
+
52
+ t.context.logger.child.returns(childLogger);
53
+
54
+ wrappedHandler(event, metadata);
55
+
56
+ Sinon.assert.calledOnceWithExactly(logger.child, { eventId: metadata.properties.headers.eventId, routingKey: metadata.fields.routingKey, minionWorkerName });
57
+
58
+ Sinon.assert.calledOnceWithExactly(handler, event, metadata, { logger: childLogger });
59
+
60
+ t.pass();
61
+ });
62
+
63
+ Test('createLoggerContext does not fail if the logger fails', (t) => {
64
+
65
+ const { context: { logger, event, metadata, minionWorkerName, handler } } = t;
66
+
67
+ const wrappedHandler = createLoggerContext(logger, minionWorkerName, handler);
68
+
69
+ t.context.logger.child.throws(new Error('failed to create logger'));
70
+
71
+ wrappedHandler(event, metadata);
72
+
73
+ Sinon.assert.calledOnceWithExactly(logger.child, { eventId: metadata.properties.headers.eventId, routingKey: metadata.fields.routingKey, minionWorkerName });
74
+
75
+ Sinon.assert.calledOnceWithExactly(handler, event, metadata, {});
76
+
77
+ t.pass();
78
+ });
79
+
80
+ Test('injectFieldFromEventAs adds field to the logger', (t) => {
81
+
82
+ const { context: { logger, metadata, handler } } = t;
83
+
84
+ const wrappedHandler = injectFieldFromEventAs(logger, 'encounterId', 'triageId')(handler);
85
+
86
+ const childLogger = sandbox.spy();
87
+
88
+ t.context.logger.child.returns(childLogger);
89
+
90
+ const event = {
91
+ triageId: faker.database.mongodbObjectId()
92
+ };
93
+
94
+ wrappedHandler(event, metadata);
95
+
96
+ Sinon.assert.calledOnceWithExactly(logger.child, { encounterId: event.triageId });
97
+
98
+ Sinon.assert.calledOnceWithExactly(handler, event, metadata, { logger: childLogger });
99
+
100
+ t.pass();
101
+ });
102
+
103
+ Test('injectFieldFromEventAs does not add the field to the logger if it is missing', (t) => {
104
+
105
+ const { context: { logger, metadata, handler } } = t;
106
+
107
+ const wrappedHandler = injectFieldFromEventAs(logger, 'encounterId', 'triageId')(handler);
108
+
109
+ const childLogger = sandbox.spy();
110
+
111
+ t.context.logger.child.returns(childLogger);
112
+
113
+ const event = {};
114
+
115
+ wrappedHandler(event, metadata);
116
+
117
+ Sinon.assert.calledOnceWithExactly(logger.child, { encounterId: undefined });
118
+
119
+ Sinon.assert.calledOnceWithExactly(handler, event, metadata, { logger: childLogger });
120
+
121
+ t.pass();
122
+ });
123
+
124
+
125
+ Test('addDefaultLoggingEventHandlers adds the handlers', (t) => {
126
+
127
+ const { context: { logger, rabbit, metadata } } = t;
128
+
129
+ const workerName = faker.word.sample();
130
+ const queueName = faker.word.sample();
131
+
132
+ const manifest = {
133
+ connection: {
134
+ rabbit
135
+ },
136
+ defaults: {
137
+ exchangeName: faker.word.sample()
138
+ },
139
+ workers: [
140
+ {
141
+ handler: sandbox.spy(),
142
+ config: {
143
+ name: workerName,
144
+ key: queueName
145
+ }
146
+ }
147
+ ]
148
+ };
149
+
150
+ const army = Army(manifest);
151
+
152
+ t.truthy(army);
153
+
154
+ const childLogger = {
155
+ info: sandbox.spy()
156
+ };
157
+
158
+ t.context.logger.child.returns(childLogger);
159
+
160
+ const armyName = faker.word.sample();
161
+
162
+ addDefaultLoggingEventHandlers(logger, army, armyName);
163
+
164
+ Sinon.assert.calledOnceWithExactly(logger.child, { armyName });
165
+
166
+ const event = {
167
+ data: faker.word.sample()
168
+ };
169
+
170
+ army.minions[workerName].handle(event, metadata);
171
+
172
+ const expectedMetadata = {
173
+ properties: metadata.properties,
174
+ fields: metadata.fields
175
+ };
176
+
177
+ Sinon.assert.calledOnceWithExactly(childLogger.info, { queue: workerName, event, metadata: expectedMetadata }, 'Got event in %s', armyName);
178
+ });
@@ -1,61 +0,0 @@
1
- version: 2
2
-
3
- defaults: &defaults
4
- working_directory: ~/repo
5
- docker:
6
- - image: circleci/node:12.18@sha256:cc3bed34d3606a476fcb4ef0f2c03890e7a8f2114c2d81055a7695ddbf2eefc9
7
-
8
- jobs:
9
- test:
10
- <<: *defaults
11
- steps:
12
- - checkout
13
-
14
- - restore_cache:
15
- keys:
16
- - dependency-cache-{{ checksum "package.json" }}
17
- - dependency-cache-
18
-
19
- - run: npm install
20
-
21
- - save_cache:
22
- key: dependency-cache-{{ checksum "package.json" }}
23
- paths:
24
- - ./node_modules
25
-
26
- - run:
27
- name: Run tests
28
- command: npm test
29
-
30
- - persist_to_workspace:
31
- root: ~/repo
32
- paths: .
33
-
34
- deploy:
35
- <<: *defaults
36
- steps:
37
- - attach_workspace:
38
- at: ~/repo
39
- - run:
40
- name: Authenticate with registry
41
- command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
42
- - run:
43
- name: Publish package
44
- command: npm publish
45
-
46
- workflows:
47
- version: 2
48
- test-deploy:
49
- jobs:
50
- - test:
51
- filters:
52
- tags:
53
- only: /^v.*/
54
- - deploy:
55
- requires:
56
- - test
57
- filters:
58
- tags:
59
- only: /^v.*/
60
- branches:
61
- ignore: /.*/