ac-useractionlog-connector 4.0.25

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 ADDED
@@ -0,0 +1,190 @@
1
+ # AC UserActionLog Connector
2
+ Connect an app to our userActionLog service with this module.
3
+
4
+ The module receives some log payload from backend applications, sends them to AWS firehose for processing/converting and stores the data (as original as well as converted) to S3.
5
+
6
+ [![Node.js CI](https://github.com/AdmiralCloud/ac-useractionlog-connector/actions/workflows/node.js.yml/badge.svg)](https://github.com/AdmiralCloud/c-useractionlog-connector/actions/workflows/node.js.yml)
7
+
8
+ # Breaking change in version 4 / Upgrading from version 3
9
+ + improved compatibility with AWS SDK
10
+ + use AWS_PROFILE instead of custom profile
11
+ + let SDK handle session management (refresh tokens)
12
+ + function is now a class - so initializing slightly changes
13
+
14
+ To upgrade (your local or non EC2 machines), please read the section "Local development/non EC2 instances"
15
+
16
+ # Breaking change in version 3
17
+ + works with Node16 or higher
18
+ + async/await - no callback!
19
+ + uses AWS IAM roles or AWS IAM profiles instead of IAM credentials
20
+
21
+ # Upgrading from version 1 to 2
22
+ Please note that version 1 is no longer supported. Version 2 uses AWS Kinesis Firehose, AWS Glue and AWS Athena.
23
+
24
+ The data is stored in S3 and you can export/download it from there any time.
25
+
26
+ # Usage
27
+
28
+ ```
29
+ // Init during bootstrap
30
+
31
+ const ualConnector = require('ac-useractionlog-connector')
32
+
33
+ ualConnector = new ual({
34
+ // optional config parameters (see below)
35
+ })
36
+
37
+ // now send logs like this
38
+ await ualConnector.log({ ip, processingTime, statusCode, logMeta: { body: req.body } }, req)
39
+ ```
40
+
41
+ ### Configuration options
42
+ |Parameter|Default value|Description|
43
+ |-----|----|----|
44
+ |region|eu-central-1|Region for Firehose|
45
+ |deliveryStreamName|UserActionLogs|Name of the stream in Firehose|
46
+ |applicationName|AC-UAL-AppNameNotSet|Name of the logging application|
47
+ |applicationVersion|AppVersionNotSet|Version of the logging application|
48
+
49
+ # AWS Credentials
50
+ Credentials are fetched using the approach described here:
51
+ https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html
52
+
53
+ ## Local development/non EC2 instances
54
+ AWS SDK automatically handles expired session tokens if you follow these instructions:
55
+
56
+ 1 Download this script -> https://github.com/AdmiralCloud/bashHelper/blob/master/awsConnect/getRoleSession.sh
57
+ 2 Prepare the file ~/.aws/config and add a configuration for the role (e.g. ac-api-instance-role) you want to use:
58
+ ```
59
+ // dev is the profile name of the IAM user (your IAM user) that is allowed to assume the role.
60
+ // The profile must be defined in ~/.aws/credentials
61
+
62
+ [profile ac-api-instance-role]
63
+ credential_process = /ABSOLUTE_PATH/BashHelpers/awsConnect/getRoleSession.sh "ac-api-instance-role" "dev"
64
+ ```
65
+
66
+ 3 export AWS_PROFILE=role in the project where this connector is used. You can also add it to pm2 config json.
67
+ 4 Run the script in your project that uses this connector. The SDK will handle everything else for you!
68
+
69
+
70
+ # Preparation
71
+ ## Glue
72
+ Create a manual table in Glue:
73
+
74
+ Name it "UAL-Default", select the database "useractionlog" (or create it if not existing), choose Type "Kinesis" with Location "UserActionLog" and the Kinesis URL "https://glue.eu-central-1.amazonaws.com". Select data format JSON. And then use the following fields:
75
+
76
+
77
+ | Pos | Field | Type |
78
+ | --- | --- | --- |
79
+ 1 | ip | string |
80
+ 2 | method | string |
81
+ 3 | controller | string |
82
+ 4 | action | string |
83
+ 5 | statuscode | int |
84
+ 6 | processingtime | int |
85
+ 7 | userId | int |
86
+ 8 | customerId | int |
87
+ 9 | mediaContainerId | int |
88
+ 10 | created | bigint |
89
+ 11 | createdAt | timestamp |
90
+ 12 | platform | string |
91
+ 13 | platformVersion | string |
92
+ 14 | clientVersion | string |
93
+ 15 | token | string |
94
+ 16 | clientid | string |
95
+ 17 | deviceidentifier | string |
96
+ 18 | useragent | string |
97
+ 19 | payload | string |
98
+ 20 | truncated | boolean |
99
+ 21 | response | string |
100
+
101
+
102
+ ## AWS Firehose
103
+ First you have to create a delivery stream (Firehose) in AWS Kinesis. Name it "UserActionLogs", select "Direct PUT or other sources".
104
+
105
+ On the next page, choose "Convert record format" and select "Apache Parquet" format. Use the table definition from Glue (above).
106
+
107
+ The destination should be S3 logbucket with prefix "ual/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/". The error prefixx can be "ual_errors/".
108
+
109
+ Also select S3 backup and use the logbucket and prefix "userActionLogs.
110
+
111
+ Finally, create the delivery stream.
112
+
113
+ # Analysis
114
+ To analyze your data you have to create a Athena database and query it.
115
+
116
+
117
+ ### Create the table
118
+ Before running the query below, consider the following:
119
+ + If you don't need the whole month, but only a day, remove "day" from partition and update the location with the day (day=xx)
120
+ + The fewer data is indexed, the faster the process.
121
+
122
+ ```
123
+ // Create the table in database useractionlog
124
+ CREATE EXTERNAL TABLE IF NOT EXISTS ual_202011 (
125
+ ip string,
126
+ method string,
127
+ controller string,
128
+ action string,
129
+ statusCode int,
130
+ processingtime int,
131
+ userId int,
132
+ customerId int,
133
+ mediaContainerId int,
134
+ createdAt timestamp,
135
+ platform string,
136
+ clientVersion string,
137
+ payload string,
138
+ token string,
139
+ clientId string,
140
+ deviceIdentifier string,
141
+ response boolean,
142
+ response string
143
+ userAgent string,
144
+ created bigint,
145
+ )
146
+ PARTITIONED BY (
147
+ day string,
148
+ hour string
149
+ )
150
+ STORED AS PARQUET
151
+ LOCATION 's3://logs.admiralcloud.live.fra/ual/year=2020/month=11/'
152
+ ```
153
+
154
+ After creation and before querying, you need to load the partition using the following command:
155
+
156
+ ```
157
+ MSCK REPAIR TABLE ual_202011;
158
+ ```
159
+
160
+ When querying data, please consider reducing the data to parse using the partions created above
161
+
162
+ ```
163
+ // Example Query using partitions
164
+
165
+ SELECT * FROM "useractionlog"."ual_202011"
166
+ where hour >= '14' AND hour < '15'
167
+ limit 10;
168
+
169
+ ```
170
+
171
+ # Error handling
172
+ If creating a Glue table is not working via AWS console, you can use CLI but have to edit/add field definitions etc later. So use this only as fallback!
173
+ ```
174
+ // FALLBACK
175
+ aws glue create-table \
176
+ --database-name useractionlog \
177
+ --table-input '{
178
+ "Name":"ual-default", "StorageDescriptor":{}}' \
179
+ --endpoint https://glue.eu-central-1.amazonaws.com
180
+ ```
181
+
182
+
183
+
184
+ ## Links
185
+ - [Website](https://www.admiralcloud.com/)
186
+ - [Twitter (@admiralcloud)](https://twitter.com/admiralcloud)
187
+ - [Facebook](https://www.facebook.com/MediaAssetManagement/)
188
+
189
+ ## License
190
+ Copyright mmpro
package/debug.js ADDED
@@ -0,0 +1,43 @@
1
+ const ual = require('./index')
2
+
3
+ const connector = new ual({})
4
+
5
+ const params = {
6
+ ip: '8.8.8.8',
7
+ statusCode: 200,
8
+ processingTime: 51,
9
+ userId: 123,
10
+ customerId: 147,
11
+ platform: 'APIv5',
12
+ requestPayload: {
13
+ type: 'video'
14
+ },
15
+ responsePayload: {
16
+ id: '123',
17
+ type: 'video'
18
+ }
19
+ }
20
+
21
+ const req = {
22
+ authInfo: {
23
+ token: 'token-123-abc',
24
+ clientId: 'clientId-456-111',
25
+ deviceIdentifier: 'myDevice',
26
+ clientVersion: '11.04.34455'
27
+ },
28
+ method: 'post',
29
+ options: {
30
+ controller: 'mediacontainer',
31
+ action: 'create'
32
+ },
33
+ headers: {
34
+ 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36'
35
+ },
36
+ allParams: () => {
37
+ return params.requestPayload
38
+ }
39
+ }
40
+
41
+ setInterval(async() => {
42
+ await connector.log(params, req)
43
+ }, 15000)
@@ -0,0 +1,34 @@
1
+ const globals = require('globals')
2
+
3
+ module.exports = [
4
+ {
5
+ files: ['index.js', 'test/test.js'],
6
+ languageOptions: {
7
+ ecmaVersion: 2022,
8
+ sourceType: 'module',
9
+ globals: {
10
+ ...globals.commonjs,
11
+ ...globals.es2015,
12
+ ...globals.node,
13
+ expect: 'readonly',
14
+ describe: 'readonly',
15
+ it: 'readonly'
16
+ }
17
+ },
18
+ rules: {
19
+ 'no-const-assign': 'error',
20
+ 'space-before-function-paren': 'off',
21
+ 'no-extra-semi': 'off',
22
+ 'object-curly-spacing': ['error', 'always'],
23
+ 'brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
24
+ 'block-spacing': 'error',
25
+ 'no-useless-escape': 'off',
26
+ 'no-console': ['warn', { allow: ['warn', 'error'] }],
27
+ 'no-unused-vars': 'error',
28
+ 'eqeqeq': 'error',
29
+ 'no-var': 'error',
30
+ 'curly': 'error',
31
+ 'prefer-const': ['error', { ignoreReadBeforeAssign: true }]
32
+ }
33
+ }
34
+ ]
package/index.js ADDED
@@ -0,0 +1,167 @@
1
+ const _ = require('lodash')
2
+
3
+ const { FirehoseClient, PutRecordCommand } = require("@aws-sdk/client-firehose")
4
+ const https = require('https')
5
+ const agent = new https.Agent({
6
+ keepAlive: true
7
+ })
8
+
9
+ // ATTN: export AWS_PROFILE=ac-api-instance-role (or whatever role is used)
10
+
11
+ class FirehoseManager {
12
+ constructor({ region = 'eu-central-1', deliveryStreamName = 'UserActionLogs', applicationName= 'AC-UAL-AppNameNotSet', applicationVersion = 'AppVersionNotSet', debug }) {
13
+ this.debug = debug
14
+ const awsParams = {
15
+ region,
16
+ httpOptions: { agent }
17
+ }
18
+
19
+ this.firehose = {}
20
+ this.firehose.deliveryStreamName = deliveryStreamName
21
+ this.firehose.applicationName = applicationName
22
+ this.firehose.applicationVersion = applicationVersion
23
+ this.firehose.client = new FirehoseClient(awsParams)
24
+ }
25
+
26
+ async microTimeString () {
27
+ const time = new Date().getTime()
28
+ const hrtime = process.hrtime()
29
+ const id = time + '.' + (hrtime[0] * 1e9 + hrtime[1])
30
+ return id.toString().substring(0, 20)
31
+ }
32
+
33
+ // Trim response object without deep recursion:
34
+ // - keeps all top-level keys
35
+ // - truncates arrays at top level and one level deep to max 3 items
36
+ trimData(data) {
37
+ try {
38
+ const obj = JSON.parse(data)
39
+ for (const key of Object.keys(obj)) {
40
+ if (Array.isArray(obj[key])) {
41
+ obj[key] = obj[key].slice(0, 3).map(item => {
42
+ if (item && typeof item === 'object') {
43
+ for (const k of Object.keys(item)) {
44
+ if (typeof item[k] === 'string' && item[k].length > 500) {
45
+ item[k] = item[k].slice(0, 500) + '...'
46
+ }
47
+ }
48
+ }
49
+ return item
50
+ })
51
+ }
52
+ else if (obj[key] && typeof obj[key] === 'object') {
53
+ for (const subKey of Object.keys(obj[key])) {
54
+ if (Array.isArray(obj[key][subKey])) {
55
+ obj[key][subKey] = obj[key][subKey].slice(0, 3)
56
+ }
57
+ }
58
+ }
59
+ else if (typeof obj[key] === 'string' && obj[key].length > 500) {
60
+ obj[key] = obj[key].slice(0, 500) + '...'
61
+ }
62
+ }
63
+
64
+ return JSON.stringify(obj)
65
+ }
66
+ catch {
67
+ console.error('AC-UserActionLog-Connector | Failed to parse JSON for trimming')
68
+ return null
69
+ }
70
+ }
71
+
72
+ async log(params, req) {
73
+ const created = parseInt(this.microTimeString())
74
+ const platform = _.toLower(_.get(params, 'platform', this.firehose.applicationName))
75
+ const platformVersion = _.toLower(_.get(params, 'platformVersion', this.firehose.applicationVersion))
76
+
77
+ const requestParams = _.get(params, 'requestPayload', req && req.allParams())
78
+
79
+ const obscureFields = [
80
+ { field: 'password', search: /.+/g, replace: 'XXXX-XXXX' }
81
+ ]
82
+
83
+ _.forEach(obscureFields, field => {
84
+ if (_.get(requestParams, field.field)) { _.set(requestParams, field.field, _.get(requestParams, field.field).replace(field.search, field.replace)) }
85
+ })
86
+
87
+ let mediaContainerId = _.get(params, 'mediaContainerId') || _.get(requestParams, 'mediaContainerId') || _.get(params, 'responsePayload.mediaContainerId')
88
+ if (!mediaContainerId && _.get(req, 'options.controller') === 'mediacontainer' && _.get(req, 'options.action') === 'create') { mediaContainerId = _.get(params, 'responsePayload.id') }
89
+
90
+ const clientId = _.get(req, 'authInfo.clientId', _.get(requestParams, 'client_id'))
91
+ const deviceIdentifier = _.get(req, 'authInfo.deviceIdentifier', _.get(requestParams, 'device'))
92
+ const clientVersion = _.get(req, 'authInfo.clientVersion', _.get(requestParams, 'clientVersion'))
93
+ const loginuid = _.get(params, 'loginuid')
94
+
95
+ const now = new Date()
96
+ const ualPayload = {
97
+ ip: _.get(params, 'ip'),
98
+ iso2: _.get(params, 'iso2'),
99
+ method: _.get(req, 'method'),
100
+ controller: _.get(req, 'options.controller'),
101
+ action: _.get(req, 'options.action'),
102
+ statusCode: _.get(params, 'statusCode'),
103
+ processingTime: _.get(params, 'processingTime'),
104
+ userId: _.get(req, 'user.id', _.get(params, 'userId')),
105
+ customerId: _.get(req, 'user.customerId', _.get(params, 'customerId')),
106
+ accessKey: _.get(params, 'accessKey'),
107
+ mediaContainerId,
108
+ created,
109
+ createdAt: now.toISOString().slice(0,10) + ' ' + now.toISOString().slice(11,23),
110
+ platform,
111
+ platformVersion,
112
+ clientVersion,
113
+ loginuid,
114
+ clientId,
115
+ deviceIdentifier,
116
+ userAgent: _.get(req, 'headers.user-agent'),
117
+ responseLength: 0,
118
+ payload: JSON.stringify(requestParams),
119
+ response: JSON.stringify(_.get(params, 'responsePayload'))
120
+ }
121
+
122
+ const recordLength = Buffer.byteLength(JSON.stringify(ualPayload), 'utf8')
123
+
124
+ if (recordLength > 1024000) {
125
+ const t0 = performance.now()
126
+ ualPayload.truncatedResponse = true
127
+ ualPayload.response = this.trimData(ualPayload.response)
128
+
129
+ // reduce request payload if still too large after trimming response
130
+ if (Buffer.byteLength(JSON.stringify(ualPayload), 'utf8') > 1024000) {
131
+ ualPayload.payload = this.trimData(ualPayload.payload)
132
+ }
133
+
134
+ // final fallback if still too large - drop response entirely but keep request payload
135
+ if (Buffer.byteLength(JSON.stringify(ualPayload), 'utf8') > 1024000) {
136
+ ualPayload.response = null
137
+ }
138
+
139
+ console.warn('AC-UserActionLog-Connector | Took %sms | Record truncated from %s to %s bytes', (performance.now() - t0).toFixed(2), recordLength, Buffer.byteLength(JSON.stringify(ualPayload), 'utf8'))
140
+ }
141
+
142
+ if (process.env.NODE_ENV === 'test') {
143
+ return ualPayload
144
+ }
145
+
146
+ const awsParams = {
147
+ DeliveryStreamName: this.firehose.deliveryStreamName,
148
+ Record: {
149
+ Data: Buffer.from(JSON.stringify(ualPayload))
150
+ }
151
+ }
152
+ const command = new PutRecordCommand(awsParams)
153
+ try {
154
+ const r = await this.firehose.client.send(command)
155
+ if (this.debug) {
156
+ const c = await this.firehose.client.config.credentials()
157
+ console.warn('UAL | FIREHOSE', c?.accessKeyId, c?.expiration, r?.$metadata?.httpStatusCode)
158
+ }
159
+ }
160
+ catch(e) {
161
+ console.error('AC-UserActionLog-Connector | Failed %j', e?.message)
162
+ }
163
+ }
164
+ }
165
+
166
+
167
+ module.exports = FirehoseManager
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "ac-useractionlog-connector",
3
+ "author": "Mark Poepping (https://www.admiralcloud.com)",
4
+ "license": "MIT",
5
+ "repository": "admiralcloud/ac-useractionlog-connector",
6
+ "version": "4.0.25",
7
+ "dependencies": {
8
+ "@aws-sdk/client-firehose": "^3.1014.0",
9
+ "@eslint/js": "^10.0.1",
10
+ "globals": "^17.4.0",
11
+ "install": "^0.13.0",
12
+ "lodash": "^4.17.23"
13
+ },
14
+ "devDependencies": {
15
+ "ac-jenkins": "git+ssh://git@ac-jenkins.github.com/admiralcloud/ac-jenkins#v1.2.10",
16
+ "ac-semantic-release": "^0.4.10",
17
+ "chai": "^4.5.0",
18
+ "eslint": "^10.1.0",
19
+ "mocha": "^11.7.5"
20
+ },
21
+ "scripts": {
22
+ "test": "NODE_ENV=test mocha --reporter spec"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "resolutions": {
28
+ "mocha/chokidar/braces": "^3.0.3",
29
+ "mocha/diff": "^8.0.3",
30
+ "minimatch": "^10.2.1",
31
+ "fast-xml-parser": "^5.3.8",
32
+ "mocha/serialize-javascript": "^7.0.3"
33
+ },
34
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
35
+ }