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 +9 -0
- package/.eslintrc.js +27 -0
- package/.github/workflows/node.js.yml +36 -0
- package/CHANGELOG.md +9 -0
- package/Makefile +16 -0
- package/README.md +71 -0
- package/index.js +98 -0
- package/package.json +27 -0
- package/test/test.js +157 -0
package/.acsemver.js
ADDED
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
|
+
[](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
|
+
})
|