ac-support-connector 0.0.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/.acsemver.js ADDED
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ repository: {
3
+ url: 'https://github.com/admiralcloud/ac-support-connector'
4
+ },
5
+ changelogFile: __dirname + '/CHANGELOG.md',
6
+ sections: [
7
+ {name: 'Connector' }
8
+ ]
9
+ };
package/.eslintrc.js ADDED
@@ -0,0 +1,27 @@
1
+ const config = {
2
+ root: true,
3
+ 'env': {
4
+ 'commonjs': true,
5
+ 'es6': true,
6
+ 'node': true
7
+ },
8
+ 'extends': 'eslint:recommended',
9
+ "rules": {
10
+ "space-before-function-paren": 0,
11
+ "no-extra-semi": 0,
12
+ "object-curly-spacing": ["error", "always"],
13
+ "brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
14
+ "no-useless-escape": 0,
15
+ "standard/no-callback-literal": 0,
16
+ "new-cap": 0
17
+ },
18
+ globals: {
19
+ describe: true,
20
+ it: true
21
+ },
22
+ 'parserOptions': {
23
+ 'ecmaVersion': 2021
24
+ },
25
+ }
26
+
27
+ module.exports = config
@@ -0,0 +1,36 @@
1
+ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Node.js CI
5
+
6
+ on:
7
+ push:
8
+ branches: [ master ]
9
+ pull_request:
10
+ branches: [ master ]
11
+
12
+ jobs:
13
+ build:
14
+
15
+ runs-on: ubuntu-latest
16
+
17
+ strategy:
18
+ matrix:
19
+ node-version: [16.x, 18.x]
20
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21
+ redis-version: [6]
22
+
23
+ steps:
24
+ - uses: actions/checkout@v3
25
+ - name: Use Node.js ${{ matrix.node-version }}
26
+ uses: actions/setup-node@v3
27
+ with:
28
+ node-version: ${{ matrix.node-version }}
29
+
30
+ - name: Start Redis
31
+ uses: supercharge/redis-github-action@1.4.0
32
+ with:
33
+ redis-version: ${{ matrix.redis-version }}
34
+
35
+ - run: yarn install
36
+ - run: yarn run test
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ <a name="0.0.1"></a>
2
+
3
+ ## [0.0.1](https://github.com/admiralcloud/ac-support-connector/compare/..v0.0.1) (2022-12-23 13:54:01)
4
+
5
+
6
+ ### Bug Fix
7
+
8
+ * **Connector:** Initial version | MP | [a6ceabb22ef634818911ad41daebcd82e0f8dbfb](https://github.com/admiralcloud/ac-support-connector/commit/a6ceabb22ef634818911ad41daebcd82e0f8dbfb)
9
+ Initial version
package/Makefile ADDED
@@ -0,0 +1,16 @@
1
+ MOCHA_OPTS= --slow 0 -A
2
+ REPORTER = spec
3
+
4
+ lint-fix:
5
+ ./node_modules/.bin/eslint --fix index.js test/test.js
6
+
7
+ lint-check:
8
+ ./node_modules/.bin/eslint index.js test/test.js
9
+
10
+ commit:
11
+ @node ./node_modules/ac-semantic-release/lib/commit.js
12
+
13
+ release:
14
+ @node ./node_modules/ac-semantic-release/lib/release.js
15
+
16
+ .PHONY: check
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # AC Support Connector
2
+ This module sends unified support payloads to SQS. A dedicated service (not part of the connector) then processes those SQS messages and delivers the support messages to different channels (e.g. Teams, a helpdesk, etc)
3
+
4
+ [![Node.js CI](https://github.com/AdmiralCloud/ac-geoip/actions/workflows/node.js.yml/badge.svg)](https://github.com/AdmiralCloud/ac-support-connector/actions/workflows/node.js.yml)
5
+
6
+ # Prerequisites
7
+ Create a SQS queue (e.g. supportQueue) and use a policy that allows all (or selected) IAM users from the account to send messages to the queue:
8
+
9
+ ```
10
+ {
11
+ "Sid": "Statement1",
12
+ "Effect": "Allow",
13
+ "Principal": {
14
+ "AWS": "arn:aws:iam::ACCOUNTID:root"
15
+ },
16
+ "Action": "sqs:SendMessage",
17
+ "Resource": "arn:aws:sqs:eu-central-1:ACCOUNTID:supportQueue"
18
+ }
19
+ ```
20
+
21
+ # Usage
22
+ ```
23
+ // Init during bootstrap
24
+
25
+ const supportConnector = require('ac-support-connector')
26
+
27
+ supportConnector.init({
28
+ accountId: 'AWS AccountId',
29
+ accessKeyId: 'AWSAK',
30
+ secretAccessKey: 'AWSSECRET',
31
+ region: 'AWS REGION',
32
+ serviceName: 'my-service',
33
+ instanceId: 'abc123' // optional unique identifier (for the instance the service is running on)
34
+ sqsQueue: 'mySupportList' // optional SQS queue name, if none set, defaults to 'supportQueue'
35
+ })
36
+
37
+ // now send logs like this
38
+
39
+ supportConnector.createMessage({
40
+ subject: 'Operation failed',
41
+ text: 'The operation ABC failed due to missing parameter',
42
+ level: 'warn' // optional, if none is set, the level is info
43
+ })
44
+ ```
45
+
46
+ # Parameters
47
+ ## Init
48
+ |Parameter|Type|Usage|
49
+ |---|---|---|
50
+ |accessKeyId|string|AWS accessKeyId with permission to send to SQS queue
51
+ |secretAccessKey|string|AWS secretAccessKey
52
+ |region|string|AWS region
53
+ |serviceName|string|Name of the service sending the support ticket
54
+ |instanceId|string OPTIONAL|Unique identifier, if service runs on multiple machines
55
+ |sqsQueue|string OPTIONAL|Name of AWS SQS queue. If not send, defaults to 'supportQueue'
56
+
57
+ ## Create Message
58
+ |Parameter|Type|Usage|
59
+ |---|---|---|
60
+ |subject|string|Subject of the message
61
+ |text|string|Text of the message
62
+ |level|string|Level of the message, if not set defaults to info. Available: error, warn, debug, verbose, info
63
+ |block|integer|Seconds before the message is sent again (if the error occurs again). If Redis is not available, memory is used.
64
+
65
+
66
+ ## Links
67
+ - [Website](https://www.admiralcloud.com/)
68
+ - [Facebook](https://www.facebook.com/MediaAssetManagement/)
69
+
70
+ ## License
71
+ Copyright AdmiralCloud AG, Mark Poepping
package/index.js ADDED
@@ -0,0 +1,98 @@
1
+ const { SQSClient, SendMessageCommand } = require('@aws-sdk/client-sqs');
2
+
3
+ const getLength = require('utf8-byte-length')
4
+ const truncate = require('truncate-utf8-bytes')
5
+
6
+ const NodeCache = require('node-cache')
7
+
8
+ module.exports = {
9
+
10
+ aws: {},
11
+
12
+
13
+ init: function({ accessKeyId, secretAccessKey, region, accountId, serviceName, instanceId, sqsQueue, redisInstance, debug }) {
14
+ // check requirements
15
+ if (!debug) {
16
+ if (!accessKeyId) return { message: 'accessKeyId_required' }
17
+ if (!secretAccessKey) return { message: 'secretAccessKey_required' }
18
+ if (!region) return { message: 'region_required' }
19
+ if (!accountId) return { message: 'accountId_required' }
20
+ }
21
+
22
+ if (accessKeyId && secretAccessKey) {
23
+ let awsParams = {
24
+ credentials: {
25
+ accessKeyId,
26
+ secretAccessKey
27
+ },
28
+ region: region || 'eu-central-1'
29
+ }
30
+ this.aws = {
31
+ sqs: new SQSClient(awsParams),
32
+ region,
33
+ accountId
34
+ }
35
+ }
36
+
37
+ this.serviceName = serviceName || 'SupportConnector'
38
+ this.instanceId = instanceId
39
+ this.sqsQueue = sqsQueue || 'supportQueue'
40
+ this.redisInstance = redisInstance
41
+ this.cache = new NodeCache()
42
+ },
43
+
44
+
45
+ createMessage: async function({ subject, text, level, block }) {
46
+ // check block time in Redis, fallback to memory
47
+ if (block) {
48
+ if (block < 1) block = 1 // make sure to use reasonable value!
49
+ const key = `${process.env.NODE_ENV}:supportConnector:${this.serviceName}:${Buffer.from(subject).toString('base64')}`
50
+ if (this.redisInstance) {
51
+ // OK if set for the first time, null if existing
52
+ let response = await this.redisInstance.set(key, 1, 'ex', block, 'nx')
53
+ if (response !== 'OK') {
54
+ return// { statusCode: 423, message: 'keyBlocked' }
55
+ }
56
+ }
57
+ else {
58
+ if (!this.cache.has(key)) {
59
+ this.cache.set(key, 1, block)
60
+ }
61
+ else {
62
+ return //
63
+ }
64
+ }
65
+ }
66
+
67
+ const messagePayload = {
68
+ subject,
69
+ text,
70
+ level
71
+ }
72
+ const maxSize = 256 * 1024 // 256kb
73
+ const recordLength = getLength(JSON.stringify(messagePayload))
74
+ if (recordLength > maxSize) {
75
+ // text must be truncated
76
+ const textLength = getLength(messagePayload.text)
77
+ const truncatedSize = textLength - (recordLength - maxSize)
78
+ messagePayload.text = truncate(messagePayload.text, truncatedSize)
79
+ console.log('AC-Support-Connector | Subject %s | Truncated to %s', subject, truncatedSize)
80
+ }
81
+
82
+ if (process.env.NODE_ENV === 'test') {
83
+ return messagePayload
84
+ }
85
+
86
+ const awsInput = {
87
+ QueueUrl: `https://sqs.${this.aws.region}.amazonaws.com/${this.aws.accountId}/${this.sqsQueue}`,
88
+ MessageBody: JSON.stringify(messagePayload)
89
+ }
90
+ const command = new SendMessageCommand(awsInput)
91
+ try {
92
+ await this.aws.sqs.send(command)
93
+ }
94
+ catch(e) {
95
+ console.log('AC-Support-Connector | Subject %s | Failed %j', subject, e?.message)
96
+ }
97
+ }
98
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "ac-support-connector",
3
+ "author": "Mark Poepping (https://www.admiralcloud.com)",
4
+ "license": "MIT",
5
+ "repository": "admiralcloud/ac-support-connector",
6
+ "version": "0.0.1",
7
+ "dependencies": {
8
+ "@aws-sdk/client-sqs": "^3.236.0",
9
+ "node-cache": "^5.1.2",
10
+ "truncate-utf8-bytes": "^1.0.2",
11
+ "utf8-byte-length": "^1.0.4"
12
+ },
13
+ "devDependencies": {
14
+ "ac-semantic-release": "^0.3.4",
15
+ "chai": "^4.3.7",
16
+ "eslint": "^8.30.0",
17
+ "expect": "^29.3.1",
18
+ "ioredis": "^5.2.4",
19
+ "mocha": "^10.2.0"
20
+ },
21
+ "scripts": {
22
+ "test": "NODE_ENV=test mocha --reporter spec || :"
23
+ },
24
+ "engines": {
25
+ "node": ">=16.0.0"
26
+ }
27
+ }
package/test/test.js ADDED
@@ -0,0 +1,157 @@
1
+ const { expect } = require('chai');
2
+
3
+ const getLength = require("utf8-byte-length")
4
+ const truncate = require("truncate-utf8-bytes")
5
+
6
+ const Redis = require("ioredis");
7
+ const redisInstance = new Redis()
8
+
9
+ const supCon = require('../index')
10
+
11
+ const string1001 = new Array(512000).join('b')
12
+
13
+ function timeout(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms));
15
+ }
16
+
17
+
18
+ describe('Basic tests', () => {
19
+ it('Test properties', async() => {
20
+ let params = {
21
+ subject: 'Failed',
22
+ text: 'This failed',
23
+ level: 'warn'
24
+ }
25
+ let response = await supCon.createMessage(params)
26
+ Object.keys(params).forEach(prop => {
27
+ const val = params[prop]
28
+ expect(response).to.have.property(prop, val)
29
+ })
30
+ })
31
+
32
+ it('Check truncated payload > 256KB - truncated', async() => {
33
+ let params = {
34
+ subject: 'Failed',
35
+ text: string1001
36
+ }
37
+ const maxSize = 256 * 1024 // 256kb
38
+ const recordLength = getLength(JSON.stringify(params))
39
+ const textLength = getLength(params.text)
40
+ const truncated = truncate(params.text, textLength - (recordLength - maxSize))
41
+ let response = await supCon.createMessage(params)
42
+ expect(response.text).to.equal(truncated)
43
+ })
44
+
45
+ it('Check init - missing params', async() => {
46
+ let initParams = {
47
+ secretAccessKey: 'XXX',
48
+ }
49
+ let response = supCon.init(initParams)
50
+ expect(response.message).to.equal('accessKeyId_required')
51
+ })
52
+
53
+ })
54
+
55
+ describe('Block with Redis', () => {
56
+ it('Init with Redis', async() => {
57
+ const initParams = {
58
+ redisInstance,
59
+ debug: true
60
+ }
61
+ supCon.init(initParams)
62
+ })
63
+
64
+ it('Send message - should work', async() => {
65
+ let params = {
66
+ subject: 'Failed',
67
+ text: 'This failed',
68
+ level: 'warn',
69
+ block: 5
70
+ }
71
+ let response = await supCon.createMessage(params)
72
+ expect(response).to.have.property('subject', params.subject)
73
+ })
74
+
75
+ it('Send message - should be blocked', async() => {
76
+ let params = {
77
+ subject: 'Failed',
78
+ text: 'This failed',
79
+ level: 'warn',
80
+ block: 5
81
+ }
82
+ let response = await supCon.createMessage(params)
83
+ expect(response).to.be.undefined
84
+ })
85
+
86
+ it('Wait for timeout', async() => {
87
+ await timeout(5000)
88
+ }).timeout(10000)
89
+
90
+ it('Send message - should work again', async() => {
91
+ let params = {
92
+ subject: 'Failed',
93
+ text: 'This failed',
94
+ level: 'warn',
95
+ block: 5
96
+ }
97
+ let response = await supCon.createMessage(params)
98
+ expect(response).to.have.property('subject', params.subject)
99
+ })
100
+ })
101
+
102
+
103
+ describe('Block with Memory', () => {
104
+
105
+ it('Init', () => {
106
+ supCon.init({
107
+ debug: true
108
+ })
109
+ })
110
+
111
+ it('Send message - should work', async() => {
112
+ let params = {
113
+ subject: 'Failed',
114
+ text: 'This failed',
115
+ level: 'warn',
116
+ block: 5
117
+ }
118
+ let response = await supCon.createMessage(params)
119
+ expect(response).to.have.property('subject', params.subject)
120
+ })
121
+
122
+ it('Send message - should be blocked', async() => {
123
+ let params = {
124
+ subject: 'Failed',
125
+ text: 'This failed',
126
+ level: 'warn',
127
+ block: 5
128
+ }
129
+ let response = await supCon.createMessage(params)
130
+ expect(response).to.be.undefined
131
+ })
132
+
133
+
134
+ it('Wait for timeout', async() => {
135
+ await timeout(5000)
136
+ }).timeout(10000)
137
+
138
+ it('Send message - should work again', async() => {
139
+ let params = {
140
+ subject: 'Failed',
141
+ text: 'This failed',
142
+ level: 'warn',
143
+ block: 5
144
+ }
145
+ let response = await supCon.createMessage(params)
146
+ expect(response).to.have.property('subject', params.subject)
147
+ })
148
+
149
+
150
+ })
151
+
152
+
153
+ describe('Cleanup', () => {
154
+ it('Close Redis', async() => {
155
+ redisInstance.quit()
156
+ })
157
+ })