• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

janis-commerce / log / 29280214079

13 Jul 2026 07:52PM UTC coverage: 97.436% (+0.05%) from 97.391%
29280214079

push

github

jormaechea
5.4.0

102 of 108 branches covered (94.44%)

Branch coverage included in aggregate %.

240 of 243 relevant lines covered (98.77%)

52.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.08
/lib/log.js
1
'use strict';
2

3
/**
4
 * @typedef {object} LogData
5
 * @property {string} [id]
6
 * @property {string} [service]
7
 * @property {string} entity
8
 * @property {string} entityId
9
 * @property {string} type
10
 * @property {string} [message]
11
 * @property {string} [client]
12
 * @property {string} [userCreated]
13
 * @property {Date} [dateCreated]
14
 * @property {object} log
15
 */
16

17
/**
18
 * This callback is displayed as part of the Requester class.
19
 * @callback LogEventEmitterCallback
20
 * @param {Array<LogData>} failedLogs
21
 * @param {LogError} error
22
 * @returns {void}
23
 */
24

25
const { ApiSession } = require('@janiscommerce/api-session');
1✔
26
const Events = require('@janiscommerce/events');
1✔
27

28
const { default: axios } = require('axios');
1✔
29
const crypto = require('crypto');
1✔
30
const logger = require('lllog')();
1✔
31

32
const logValidator = require('./log-validator');
1✔
33

34
const LogError = require('./log-error');
1✔
35

36
const serverlessConfiguration = require('./serverless-configuration');
1✔
37
const FirehoseInstance = require('./firehose-instance');
1✔
38
const LogTracker = require('./log-tracker');
1✔
39

40
const { getTracePrivateFields, hideFieldsFromLog } = require('./helpers/utils');
1✔
41
const { shouldAddEndedListener, endedListenerWasAdded } = require('./helpers/events');
1✔
42

43
const MAX_LOGS_PER_LOCAL_BATCH = 100;
1✔
44

45
// Sentinel client value used for core logs (logs for entities that don't belong to any client).
46
const CORE_CLIENT = '__core__';
1✔
47

48
module.exports = class Log {
1✔
49

50
        /**
51
         * The sentinel client value used to identify core logs (logs for entities without a client).
52
         *
53
         * @static
54
         * @returns {string}
55
         */
56
        static get CORE_CLIENT() {
57
                return CORE_CLIENT;
7✔
58
        }
59

60
        /**
61
         * Returns the sls helpers needed for serverless configuration.
62
         *
63
         * @static
64
         * @returns {Array}
65
         */
66
        static get serverlessConfiguration() {
67
                return serverlessConfiguration();
1✔
68
        }
69

70
        static get serviceName() {
71
                return process.env.JANIS_SERVICE_NAME;
133✔
72
        }
73

74
        static shouldAddLogs() {
75
                return ['beta', 'qa', 'prod'].includes(process.env.JANIS_ENV);
38✔
76
        }
77

78
        /**
79
         * Send logs to Local extension server or Firehose, based on JANIS_TRACE_EXTENSION_ENABLED env var
80
         *
81
         * @static
82
         * @param {string} client The client code who created the log. If a log contains the field client, this param will be overridden, allowing to insert logs from multiple clients together.
83
         * @param {LogData|Array.<LogData>} logs The log object or log objects array
84
         * @returns {Promise<void|{ successCount: number, failedCount: number }>} Resolves with Firehose stats when sent to Firehose; otherwise void for the local extension path.
85
         *
86
         * @example
87
         * Log.add('some-client', {
88
         *         type: 'some-type',
89
         *         entity: 'some-entity',
90
         *         entityId: 'some-entityId',
91
         *         message: 'some-message',
92
         *         log: {
93
         *                 some: 'log'
94
         *         }
95
         * });
96
         */
97
        static async add(client, logs) {
98

99
                if(!this.shouldAddLogs())
38✔
100
                        return;
2✔
101

102
                this.start();
36✔
103

104
                if(!Array.isArray(logs))
36✔
105
                        logs = [logs];
33✔
106

107
                logs = this.getValidatedLogs(logs, client);
36✔
108

109
                if(!logs.length)
36✔
110
                        return;
10✔
111

112
                return this.shouldAddLogsLocally(logs)
26✔
113
                        ? this.addLogsLocally(logs)
114
                        : this.sendToTrace(logs);
115
        }
116

117
        /**
118
         * Send core logs (entities without a client) to Local extension server or Firehose, using the sentinel CORE_CLIENT value.
119
         * Unlike add(), this FORCES the client to CORE_CLIENT on every log, overriding any client field present on the logs
120
         * (a core log has no client by definition, so a caller must not be able to route it to a real client).
121
         *
122
         * @static
123
         * @param {LogData|Array.<LogData>} logs The log object or log objects array
124
         * @returns {Promise<void|{ successCount: number, failedCount: number }>} Resolves with Firehose stats when sent to Firehose; otherwise void for the local extension path.
125
         *
126
         * @example
127
         * Log.addCore({
128
         *         type: 'some-type',
129
         *         entity: 'some-core-entity',
130
         *         entityId: 'some-entityId',
131
         *         message: 'some-message',
132
         *         log: {
133
         *                 some: 'log'
134
         *         }
135
         * });
136
         */
137
        static async addCore(logs) {
138
                const toCore = log => ({ ...log, client: CORE_CLIENT });
6✔
139
                return this.add(CORE_CLIENT, Array.isArray(logs) ? logs.map(toCore) : toCore(logs));
5✔
140
        }
141

142
        static start() {
143

144
                // this method is called in janiscommerce packages handlers to ensure end layer extension (even when no logs loaded in function)
145

146
                if(this.traceExtensionIsEnabled() && shouldAddEndedListener()) {
36✔
147
                        Events.on('janiscommerce.ended', this.flushLogsOnEnded);
6✔
148
                        endedListenerWasAdded();
6✔
149
                }
150
        }
151

152
        static getValidatedLogs(logs, client) {
153

154
                const privateFields = getTracePrivateFields();
36✔
155

156
                return logs.map(log => {
36✔
157

158
                        try {
157✔
159

160
                                log = this.preFormatLog(log, client, privateFields);
157✔
161

162
                                const result = logValidator(log);
156✔
163

164
                                if(result !== true)
156✔
165
                                        throw new LogError(result[0].message, LogError.codes.INVALID_LOG);
10✔
166

167
                                return log;
146✔
168

169
                        } catch(err) {
170
                                logger.error(`Validation Error while creating Trace logs - ${err.message}`);
11✔
171
                                return null;
11✔
172
                        }
173

174
                }).filter(Boolean);
175
        }
176

177
        static preFormatLog({ log, ...logObject }, client, privateFields) {
178

179
                const now = new Date();
157✔
180

181
                if(!logObject.id)
157✔
182
                        logObject.id = crypto.randomUUID();
1✔
183

184
                if(!logObject.service)
157✔
185
                        logObject.service = this.serviceName;
133✔
186

187
                if(!logObject.client)
157✔
188
                        logObject.client = client;
151✔
189

190
                if(!logObject.dateCreated)
157✔
191
                        logObject.dateCreated = now;
154✔
192
                else if(typeof logObject.dateCreated === 'string')
3✔
193
                        logObject.dateCreated = new Date(logObject.dateCreated);
2✔
194

195
                let logData = log || {};
157✔
196

197
                if(typeof logData === 'string')
157✔
198
                        logData = JSON.parse(logData);
1✔
199

200
                if(Array.isArray(logData))
156✔
201
                        logData = { data: logData };
2✔
202
                else
203
                        logData = { ...log };
154✔
204

205
                const functionName = process.env.JANIS_FUNCTION_NAME || process.env.AWS_LAMBDA_FUNCTION_NAME;
156✔
206
                const requestId = process.env.AWS_LAMBDA_REQUEST_ID;
156✔
207

208
                if(!logData.functionName && (functionName || requestId))
156✔
209
                        logData.functionName = requestId ? `${functionName || 'unknown'}@${requestId}` : functionName;
4✔
210

211
                if(!logData.apiRequestLogId && process.env.JANIS_API_REQUEST_LOG_ID)
156✔
212
                        logData.apiRequestLogId = process.env.JANIS_API_REQUEST_LOG_ID;
1✔
213

214
                if(privateFields)
156✔
215
                        logData = hideFieldsFromLog(logData, privateFields);
1✔
216

217
                if(Object.keys(logData).length)
156✔
218
                        logObject.log = JSON.stringify(logData);
154✔
219

220
                return logObject;
156✔
221
        }
222

223
        static shouldAddLogsLocally(logs) {
224
                return this.traceExtensionIsEnabled()
26✔
225
                        && logs.length < MAX_LOGS_PER_LOCAL_BATCH;
226
        }
227

228
        static traceExtensionIsEnabled() {
229
                return !!process.env.JANIS_TRACE_EXTENSION_ENABLED;
62✔
230
        }
231

232
        static async addLogsLocally(logs) {
233

234
                try {
6✔
235

236
                        // Local server implemented in Trace Lambda Layer
237
                        await axios.post('http://127.0.0.1:8585/logs', { logs }, { timeout: 300 });
6✔
238

239
                } catch(err) {
240

241
                        // If local server fails, go straight to Firehose
242
                        logger.error(`Failed to save ${logs.length} logs locally. Fallbacking to Firehose.`, err);
1✔
243

244
                        return this.sendToTrace(logs);
1✔
245
                }
246
        }
247

248
        /**
249
         *        Send multiple logs directly to Firehose. If you're implementing a Janis Service, you should be using the add() method
250
         *
251
         * @param {LogData[]} logs The logs. They must include the `client` property, or they will be ignored
252
         * @returns {Promise<{ successCount: number, failedCount: number }>}
253
         */
254
        static async sendToTrace(logs) {
255

256
                if(!logs.length)
22!
257
                        return;
×
258

259
                const firehoseInstance = new FirehoseInstance();
22✔
260

261
                return firehoseInstance.putRecords(logs.map(this.formatBeforeTrace));
22✔
262
        }
263

264
        static async flushLogsOnEnded() {
265

266
                try {
3✔
267

268
                        await axios.post('http://127.0.0.1:8585/end', undefined, { timeout: 1000 });
3✔
269

270
                } catch(err) {
271

272
                        logger.error('Failed calling http://127.0.0.1:8585/end', err);
1✔
273
                }
274
        }
275

276
        static formatBeforeTrace({ dateCreated, ...log }) {
277

278
                if(!dateCreated)
142!
279
                        dateCreated = new Date();
×
280
                else if(typeof dateCreated === 'string')
142!
281
                        dateCreated = new Date(dateCreated);
×
282

283
                const now = new Date().getTime();
142✔
284
                log.sendToTraceDelay = Math.ceil((now - dateCreated.getTime()) / 1000);
142✔
285

286
                log.dateCreated = dateCreated.toISOString();
142✔
287

288
                return log;
142✔
289
        }
290

291
        /**
292
         * @static
293
         * @param {string} clientCode The client code who created the log
294
         * @returns {import('./log-tracker')}
295
         */
296
        static createTracker(clientCode) {
297
                const apiSession = new ApiSession({ clientCode });
3✔
298
                return apiSession.getSessionInstance(LogTracker, this);
3✔
299
        }
300
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc