vtlab-generic-functions 1.0.41 → 1.0.43
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/lambdaClass/index.js +4 -2
- package/package.json +2 -1
- package/telemetry/index.js +407 -0
package/lambdaClass/index.js
CHANGED
|
@@ -18,14 +18,16 @@ module.exports = class Lambda {
|
|
|
18
18
|
ftpConnection;
|
|
19
19
|
jobsByTrackingId;
|
|
20
20
|
DEBUG;
|
|
21
|
+
context;
|
|
21
22
|
|
|
22
23
|
results = {
|
|
23
24
|
totalError: 0, totalSuccess: 0, error: [], success: [],
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
constructor({ carrier, event, DEBUG = false }) {
|
|
27
|
+
constructor({ carrier, event, DEBUG = false, context }) {
|
|
27
28
|
this.DEBUG = false;
|
|
28
29
|
this.carrier = carrier;
|
|
30
|
+
this.context = context;
|
|
29
31
|
if (event.body) {
|
|
30
32
|
//Locally we receive event.body, event.body needs to be parsed. In AWS all fields are into event. (event={lodId:'...', payload:[]})
|
|
31
33
|
event = JSON.parse(event.body);
|
|
@@ -196,7 +198,7 @@ module.exports = class Lambda {
|
|
|
196
198
|
};
|
|
197
199
|
|
|
198
200
|
logMessage = async (message) => {
|
|
199
|
-
await cloudwatch.putLogEvents(`${this.logUuid} ${message}`, this.logGroupName, 2, this.logStreamName);
|
|
201
|
+
await cloudwatch.putLogEvents(`${this?.context?.awsRequestId || '-'} ${this.logUuid} ${message}`, this.logGroupName, 2, this.logStreamName);
|
|
200
202
|
};
|
|
201
203
|
|
|
202
204
|
logError = async (error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vtlab-generic-functions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.43",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"test": "jest"
|
|
6
6
|
},
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"@aws-sdk/client-ses": "^3.478.0",
|
|
11
11
|
"@aws-sdk/client-sqs": "^3.478.0",
|
|
12
12
|
"@aws-sdk/s3-request-presigner": "^3.484.0",
|
|
13
|
+
"@supabase/supabase-js": "^2.110.0",
|
|
13
14
|
"axios": "^1.6.2",
|
|
14
15
|
"countries-code": "^1.1.0",
|
|
15
16
|
"country-data": "0.0.31",
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
const { createClient } = require("@supabase/supabase-js");
|
|
2
|
+
const Joi = require("joi");
|
|
3
|
+
const { randomBytes } = require("crypto");
|
|
4
|
+
|
|
5
|
+
const VALID_ENVIRONMENTS = [
|
|
6
|
+
"production",
|
|
7
|
+
"develop",
|
|
8
|
+
"demo",
|
|
9
|
+
"staging",
|
|
10
|
+
"testing",
|
|
11
|
+
"uat"
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const statusMap = {
|
|
15
|
+
error: "FAIL",
|
|
16
|
+
warning: "WARNING",
|
|
17
|
+
ok: "SUCCESS"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let defaultClient;
|
|
21
|
+
|
|
22
|
+
const generateRandomString = (length = 10) => {
|
|
23
|
+
return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const formatError = (err) => {
|
|
27
|
+
if (!err) return "";
|
|
28
|
+
if (typeof err === "string") return err;
|
|
29
|
+
if (err.response && err.response.data) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(err.response.data);
|
|
32
|
+
} catch (_) {
|
|
33
|
+
return String(err.response.data);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (err.message) return err.message;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(err);
|
|
39
|
+
} catch (_) {
|
|
40
|
+
return String(err);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const normalizeEnvironment = (environment) => {
|
|
45
|
+
if (!environment || !VALID_ENVIRONMENTS.includes(environment)) {
|
|
46
|
+
return "develop";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return environment;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const createSupabaseAdminClient = ({ supabaseUrl, serviceRoleKey }) => {
|
|
53
|
+
if (!supabaseUrl || !serviceRoleKey) {
|
|
54
|
+
throw new Error("Telemetry Supabase configuration missing: explicit supabaseUrl and serviceRoleKey are required");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return createClient(supabaseUrl, serviceRoleKey, {
|
|
58
|
+
auth: {
|
|
59
|
+
autoRefreshToken: true,
|
|
60
|
+
persistSession: true
|
|
61
|
+
},
|
|
62
|
+
global: {
|
|
63
|
+
headers: {
|
|
64
|
+
apikey: serviceRoleKey,
|
|
65
|
+
Authorization: `Bearer ${serviceRoleKey}`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const chunkArray = (items, chunkSize) => {
|
|
72
|
+
const chunks = [];
|
|
73
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
74
|
+
chunks.push(items.slice(i, i + chunkSize));
|
|
75
|
+
}
|
|
76
|
+
return chunks;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const parseContentBeforeSaving = (record) => {
|
|
80
|
+
return {
|
|
81
|
+
uuid: record.uuid,
|
|
82
|
+
environment: record.environment,
|
|
83
|
+
process_run_id: record.processId,
|
|
84
|
+
thread_id: record.threadId || null,
|
|
85
|
+
status: statusMap[record.status],
|
|
86
|
+
created_at: record.date,
|
|
87
|
+
message: record.message,
|
|
88
|
+
last_event: record.lastEvent,
|
|
89
|
+
details: record.details || {}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const createTelemetryClient = (options = {}) => {
|
|
94
|
+
let runtimeConfig;
|
|
95
|
+
let supabaseClient;
|
|
96
|
+
const logger = options.logger || console;
|
|
97
|
+
|
|
98
|
+
const resolveRuntimeConfig = async () => {
|
|
99
|
+
if (runtimeConfig) return runtimeConfig;
|
|
100
|
+
|
|
101
|
+
runtimeConfig = {
|
|
102
|
+
environment: normalizeEnvironment(options.environment),
|
|
103
|
+
supabaseUrl: options.supabaseUrl,
|
|
104
|
+
serviceRoleKey: options.serviceRoleKey
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return runtimeConfig;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getSupabaseClient = async () => {
|
|
111
|
+
if (supabaseClient) return supabaseClient;
|
|
112
|
+
|
|
113
|
+
if (options.supabaseClient) {
|
|
114
|
+
supabaseClient = options.supabaseClient;
|
|
115
|
+
return supabaseClient;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const resolvedConfig = await resolveRuntimeConfig();
|
|
119
|
+
supabaseClient = createSupabaseAdminClient({
|
|
120
|
+
supabaseUrl: resolvedConfig.supabaseUrl,
|
|
121
|
+
serviceRoleKey: resolvedConfig.serviceRoleKey
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return supabaseClient;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const findProcessId = async (name, account, environment) => {
|
|
128
|
+
if (!account) {
|
|
129
|
+
logger.log(`No account provided to find processId in supabase for process ${name} in environment ${environment}`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const client = await getSupabaseClient();
|
|
135
|
+
let query = client.from("processes").select("id").eq("name", name).eq("account", account);
|
|
136
|
+
if (environment) {
|
|
137
|
+
query = query.eq("environment", environment);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { data: processId, error: processIdError } = await query
|
|
141
|
+
.order("created_at", { ascending: false })
|
|
142
|
+
.limit(1)
|
|
143
|
+
.maybeSingle();
|
|
144
|
+
|
|
145
|
+
if (processIdError) {
|
|
146
|
+
logger.log(`Error finding processId in supabase: ${formatError(processIdError)} for process ${name} in account ${account} in environment ${environment}`);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return processId ? processId.id : false;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
logger.log(`Error finding processId in supabase: ${formatError(e)} for process ${name} in account ${account} in environment ${environment}`);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const createProcess = async (params) => {
|
|
158
|
+
const { processName, account, environment, logGroup, logStream } = params;
|
|
159
|
+
|
|
160
|
+
if (!account) {
|
|
161
|
+
logger.log("No account provided to create process in supabase", processName);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const client = await getSupabaseClient();
|
|
167
|
+
let { data: processId, error: processIdError } = await client
|
|
168
|
+
.from("processes")
|
|
169
|
+
.insert({
|
|
170
|
+
name: processName,
|
|
171
|
+
description: `Process ${processName}`,
|
|
172
|
+
account,
|
|
173
|
+
environment,
|
|
174
|
+
log_group: logGroup,
|
|
175
|
+
log_stream: logStream,
|
|
176
|
+
})
|
|
177
|
+
.select("id")
|
|
178
|
+
.single();
|
|
179
|
+
|
|
180
|
+
if (processIdError) {
|
|
181
|
+
logger.log(`Error creating process in supabase: ${formatError(processIdError)} for process ${processName} in account ${account} in environment ${environment}`);
|
|
182
|
+
processId = await findProcessId(processName, account, environment);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!processId) {
|
|
186
|
+
logger.log(`Error finding processId in supabase for process ${processName} in account ${account} in environment ${environment}`);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return processId.id || processId;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
logger.log(`Error creating process in supabase: ${formatError(e)} for process ${processName} in account ${account} in environment ${environment}`);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const createProcessRun = async (processId, params) => {
|
|
198
|
+
if (!processId) {
|
|
199
|
+
throw new Error(`Error creating process run in supabase: processId is required but got ${processId}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const client = await getSupabaseClient();
|
|
203
|
+
let body = {
|
|
204
|
+
uuid: params.uuid || params.logExecutionUUID || generateRandomString(10),
|
|
205
|
+
environment: params.environment,
|
|
206
|
+
account: params.account || "",
|
|
207
|
+
status: "STARTED"
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const insertProcessRun = async () => {
|
|
211
|
+
return client
|
|
212
|
+
.from("processes_runs")
|
|
213
|
+
.insert({
|
|
214
|
+
process_id: processId,
|
|
215
|
+
...body
|
|
216
|
+
})
|
|
217
|
+
.select("id")
|
|
218
|
+
.single();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
let { data: processRunId, error: processRunIdError } = await insertProcessRun();
|
|
222
|
+
|
|
223
|
+
const duplicateConstraintMessage = 'duplicate key value violates unique constraint "processes_runs_uuid_key"';
|
|
224
|
+
if ((processRunIdError || !processRunId) && (processRunIdError && (processRunIdError.code === "23505" || (processRunIdError.message || "").includes(duplicateConstraintMessage)))) {
|
|
225
|
+
body = {
|
|
226
|
+
...body,
|
|
227
|
+
uuid: `${body.uuid}-${generateRandomString(3)}`
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
({ data: processRunId, error: processRunIdError } = await insertProcessRun());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (processRunIdError || !processRunId) {
|
|
234
|
+
throw new Error(`Error creating process run in supabase: ${formatError(processRunIdError)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
id: processRunId.id,
|
|
239
|
+
uuid: body.uuid
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const createTelemetryHeader = async (params = {}) => {
|
|
244
|
+
try {
|
|
245
|
+
const resolvedConfig = await resolveRuntimeConfig();
|
|
246
|
+
const telemetryParams = {
|
|
247
|
+
...params,
|
|
248
|
+
environment: params.environment || resolvedConfig.environment
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
let processId = await findProcessId(
|
|
252
|
+
telemetryParams.processName,
|
|
253
|
+
telemetryParams.account,
|
|
254
|
+
telemetryParams.environment
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (!processId) {
|
|
258
|
+
processId = await createProcess(telemetryParams);
|
|
259
|
+
if (!processId) {
|
|
260
|
+
logger.log("Error creating process in supabase", telemetryParams.processName, telemetryParams.account);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const processRunIdResults = await createProcessRun(processId, telemetryParams);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
processId: processRunIdResults.id,
|
|
269
|
+
uuid: processRunIdResults.uuid
|
|
270
|
+
};
|
|
271
|
+
} catch (e) {
|
|
272
|
+
logger.log("Error creating telemetry header", e);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const saveBulkEvents = async (events) => {
|
|
278
|
+
const client = await getSupabaseClient();
|
|
279
|
+
const { error: processEventsError } = await client.from("processes_events").insert(events);
|
|
280
|
+
if (processEventsError) {
|
|
281
|
+
throw new Error(`Error saving bulk events in supabase: ${formatError(processEventsError)}`);
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const sendTelemetryEvents = async (events = []) => {
|
|
287
|
+
try {
|
|
288
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const resolvedConfig = await resolveRuntimeConfig();
|
|
293
|
+
const validationSchema = Joi.object({
|
|
294
|
+
environment: Joi.string().valid(...VALID_ENVIRONMENTS).default(resolvedConfig.environment),
|
|
295
|
+
processId: Joi.string().required(),
|
|
296
|
+
uuid: Joi.string().required(),
|
|
297
|
+
date: Joi.string().isoDate().required().default(new Date().toISOString()),
|
|
298
|
+
threadId: Joi.string().allow(null).optional(),
|
|
299
|
+
message: Joi.string().max(300).truncate().required(),
|
|
300
|
+
lastEvent: Joi.boolean().default(false),
|
|
301
|
+
status: Joi.string().valid("error", "ok", "warning").default("ok"),
|
|
302
|
+
details: Joi.object().unknown(true).default({})
|
|
303
|
+
}).required();
|
|
304
|
+
|
|
305
|
+
const chunks = chunkArray(events, 10);
|
|
306
|
+
const sanitizedEvents = chunks.map((chunk) => {
|
|
307
|
+
return chunk.map((event) => {
|
|
308
|
+
try {
|
|
309
|
+
const validationResult = validationSchema.validate(event);
|
|
310
|
+
if (validationResult.error) {
|
|
311
|
+
throw validationResult.error;
|
|
312
|
+
}
|
|
313
|
+
return validationResult.value;
|
|
314
|
+
} catch (e) {
|
|
315
|
+
logger.log(e);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}).filter((event) => event !== null);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
for (const chunk of sanitizedEvents) {
|
|
322
|
+
try {
|
|
323
|
+
if (chunk.length === 0) continue;
|
|
324
|
+
const processEvents = chunk.map((event) => parseContentBeforeSaving(event));
|
|
325
|
+
const processEventsSaved = await saveBulkEvents(processEvents);
|
|
326
|
+
if (!processEventsSaved) {
|
|
327
|
+
logger.log("Error saving process events in supabase", processEvents);
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
} catch (_) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return true;
|
|
336
|
+
} catch (e) {
|
|
337
|
+
logger.log("Error sending telemetry events", e);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const createTelemetryEmitter = (telemetryHeader) => {
|
|
343
|
+
let telemetryEndEmitted = false;
|
|
344
|
+
|
|
345
|
+
const normalizeMessage = (message) => {
|
|
346
|
+
if (typeof message !== "string") {
|
|
347
|
+
return "";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return message.length > 300 ? `${message.slice(0, 297)}...` : message;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const emitTelemetryEvent = async (message, status = "ok", extra = {}) => {
|
|
354
|
+
const isLastEvent = extra && extra.lastEvent === true;
|
|
355
|
+
|
|
356
|
+
if (!telemetryHeader) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (isLastEvent) {
|
|
361
|
+
telemetryEndEmitted = true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
await sendTelemetryEvents([
|
|
366
|
+
{
|
|
367
|
+
processId: telemetryHeader.processId,
|
|
368
|
+
uuid: telemetryHeader.uuid,
|
|
369
|
+
date: new Date().toISOString(),
|
|
370
|
+
message: normalizeMessage(message),
|
|
371
|
+
status,
|
|
372
|
+
...extra
|
|
373
|
+
}
|
|
374
|
+
]);
|
|
375
|
+
return true;
|
|
376
|
+
} catch (_) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
emitTelemetryEvent,
|
|
383
|
+
hasEmittedLastEvent: () => telemetryEndEmitted
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
createTelemetryHeader,
|
|
389
|
+
sendTelemetryEvents,
|
|
390
|
+
createTelemetryEmitter
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const getDefaultClient = () => {
|
|
395
|
+
if (!defaultClient) {
|
|
396
|
+
defaultClient = createTelemetryClient();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return defaultClient;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
module.exports = {
|
|
403
|
+
createTelemetryClient,
|
|
404
|
+
createTelemetryHeader: (...args) => getDefaultClient().createTelemetryHeader(...args),
|
|
405
|
+
sendTelemetryEvents: (...args) => getDefaultClient().sendTelemetryEvents(...args),
|
|
406
|
+
createTelemetryEmitter: (...args) => getDefaultClient().createTelemetryEmitter(...args)
|
|
407
|
+
};
|