ntlogger 2.5.1 → 2.6.0

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/index.js CHANGED
@@ -8,33 +8,39 @@
8
8
  * TODO: [Feature] Generic Webhook Plugin for sending logs to any webhook
9
9
  * TODO: [Feature] Email Plugin for sending logs to an email address
10
10
  * TODO: [Feature] SMS Mail ID Plugin for sending logs to a phone number
11
+ * TODO: [Feature] Allow for custom log levels and colors
11
12
  */
12
13
 
13
14
  /**
14
15
  * Change Log:
16
+ * v2.6.0 - 08/15/2024:
17
+ * [FEATURE] Teams Plugin! Send logs to a Microsoft Teams channel using incoming webhooks.
18
+ *
19
+ * v2.5.2 - 08/11/2024:
20
+ * [BUG] Fixed an issue where the /plugins/lib folder was not being pushed to npmjs.com.
15
21
  *
16
22
  * v2.5.1 - 08/10/2024:
17
- * [Bug] Fixed a bug where Jest plugin was not working due to a missing config parameter. This rule was not originally enforced until v2.5.0.
23
+ * [BUG] Fixed a bug where Jest plugin was not working due to a missing config parameter. This rule was not originally enforced until v2.5.0.
18
24
  *
19
25
  * v2.5.0 - 08/10/2024:
20
26
  * [NOTE] Syslog plugin can send logs using TLS but it is not tested. Avoid sensitive data.
21
- * [Feature] Added a plugin for Discord webhook integration, allowing log messages to be sent directly to a specified Discord channel.
22
- * [Feature] Added a plugin for Syslog server integration, enabling log messages to be sent to a Syslog server using UDP, TCP, or TLS protocols.
23
- * [Feature] Implemented clean signal handling for graceful shutdowns (SIGINT, SIGTERM).
27
+ * [FEATURE] Added a plugin for Discord webhook integration, allowing log messages to be sent directly to a specified Discord channel.
28
+ * [FEATURE] Added a plugin for Syslog server integration, enabling log messages to be sent to a Syslog server using UDP, TCP, or TLS protocols.
29
+ * [FEATURE] Implemented clean signal handling for graceful shutdowns (SIGINT, SIGTERM).
24
30
  *
25
- * [Update] @sentry/node ^8.17.0 --> ^8.25.0
26
- * [Update] mysql2 ^3.10.2 --> ^3.11.0
27
- * [Update] winston ^3.13.1 --> ^3.14.1
28
- * [Update] @sentry/profiling-node ^8.17.0 --> ^8.25.0
29
- * [Update] jest ^29.0.0 --> ^29.7.0
31
+ * [UPDATED] @sentry/node ^8.17.0 --> ^8.25.0
32
+ * [UPDATED] mysql2 ^3.10.2 --> ^3.11.0
33
+ * [UPDATED] winston ^3.13.1 --> ^3.14.1
34
+ * [UPDATED] @sentry/profiling-node ^8.17.0 --> ^8.25.0
35
+ * [UPDATED] jest ^29.0.0 --> ^29.7.0
30
36
  *
31
37
  * v2.4.0 - 08/01/2024:
32
38
  * [NOTE] Damn its been 4 months.. kinda. forgot to log last changes
33
39
  * [CRITICAL] Fixed `includes` in package.json which prevented plugins folder from being pushed to npmjs.com
34
40
  * [QA] Added test cases using jest
35
- * [Feature] Added session ID to logger meta, try `console.log(log.defaultMeta.ID)`
36
- * [Feature] Created the plugin jest. Added a custom transport for in memory logging and testing
37
- * [Feature] Created the config var `skipCache` to allow you to create a new logger instance within the same file. (Usual behavior is to return the same instance within the same file)
41
+ * [FEATURE] Added session ID to logger meta, try `console.log(log.defaultMeta.ID)`
42
+ * [FEATURE] Created the plugin jest. Added a custom transport for in memory logging and testing
43
+ * [FEATURE] Created the config var `skipCache` to allow you to create a new logger instance within the same file. (Usual behavior is to return the same instance within the same file)
38
44
  *
39
45
  * v2.2.3 - 04/01/2024:
40
46
  * Reduced GitHub Actions to only NPM publish
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntlogger",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/NightSquawk/NightTimeLogger.git"
@@ -20,12 +20,13 @@
20
20
  "sentry compatible",
21
21
  "mysql compatible",
22
22
  "syslog compatible",
23
- "discord compatible"
23
+ "discord compatible",
24
+ "teams compatible"
24
25
  ],
25
26
  "files": [
26
27
  "index.js",
27
28
  "lib/*.js",
28
- "plugins/*.js"
29
+ "plugins/**/*"
29
30
  ],
30
31
  "license": "GPL-3.0",
31
32
  "dependencies": {
@@ -0,0 +1,295 @@
1
+ # NightTimeLogger Plugins
2
+
3
+ NightTimeLogger is a powerful and flexible logging utility with various plugins to extend its functionality. This README provides an overview of the available plugins and instructions on how to set them up.
4
+
5
+ ## Available Plugins
6
+
7
+ ### 1. Discord
8
+ **Description:** Sends logs to a Discord webhook, allowing you to receive log messages directly in a specified Discord channel.
9
+
10
+ ![Discord Plugin Output](https://github.com/NightSquawk/NightTimeLogger/blob/main/images/plugins/discord/pluginDiscordOutput.png)
11
+
12
+ **Setup:**
13
+ ```javascript
14
+ {
15
+ name: 'Discord',
16
+ config: {
17
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL
18
+ avatarUrl: process.env.DISCORD_AVATAR_URL || 'https://pbs.twimg.com/profile_images/997535493624508416/V7Ed1k2o_400x400.jpg',
19
+ username: process.env.DISCORD_USERNAME || 'NTLogger - Info',
20
+ level: "info",
21
+ strict: true,
22
+ },
23
+ }
24
+ ```
25
+
26
+ ### 2. Sentry
27
+ Description: Initializes the Sentry SDK for error tracking and defines a custom Winston transport for capturing and sending error logs to Sentry.
28
+
29
+ Setup:
30
+
31
+ ``` javascript
32
+ {
33
+ name: 'Sentry',
34
+ config: {
35
+ dsn: process.env.SENTRY_DSN || null,
36
+ release: process.env.SENTRY_RELEASE || null,
37
+ tracesSampleRate: 1.0,
38
+ profilesSampleRate: 1.0,
39
+ environment: process.env.NODE_ENV || 'development',
40
+ debug: false,
41
+ attachStacktrace: true,
42
+ integrations: [
43
+ nodeProfilingIntegration()
44
+ ],
45
+ serverName: null,
46
+ maxBreadcrumbs: 100,
47
+ autoSessionTracking: true,
48
+ sessionTracking: {},
49
+ tracesSampler: null
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### 3. MySQL
55
+ Description: Stores logs in a MySQL database, allowing you to persist log messages and analyze them using SQL queries.
56
+
57
+ Setup:
58
+
59
+ ``` javascript
60
+ {
61
+ name: 'MySQL',
62
+ config: {
63
+ host: process.env.MYSQL_DB_HOST || 'localhost',
64
+ port: process.env.MYSQL_DB_PORT || 3306,
65
+ user: process.env.MYSQL_DB_USER || 'root',
66
+ password: process.env.MYSQL_DB_PASSWORD || '',
67
+ database: process.env.MYSQL_DB_NAME || 'test',
68
+ table: process.env.MYSQL_DB_TABLE || 'logs',
69
+ logLevel: process.env.LOG_LEVEL || 'info',
70
+ },
71
+ }
72
+ ````
73
+ ### 4. Jest
74
+ Description: Stores log messages in memory for testing purposes, particularly useful when running tests with Jest.
75
+
76
+ Setup:
77
+
78
+ ``` javascript
79
+ {
80
+ name: 'Jest',
81
+ }
82
+ ```
83
+
84
+ ### 5. Syslog
85
+ Description: Sends logs to a Syslog server using a custom Syslog client. This allows integration with traditional logging systems that rely on Syslog.
86
+
87
+ ![Syslog Plugin Output](https://github.com/NightSquawk/NightTimeLogger/blob/main/images/plugins/syslog/pluginSyslogOutput.png)
88
+
89
+
90
+ Setup:
91
+
92
+ ``` javascript
93
+ {
94
+ name: 'Syslog',
95
+ config: {
96
+ host: process.env.SYSLOG_HOST || 'localhost',
97
+ port: process.env.SYSLOG_PORT || 514,
98
+ protocol: process.env.SYSLOG_PROTOCOL || 'UDP', // Options: 'UDP', 'TCP', 'TLS'
99
+ rfc: process.env.SYSLOG_RFC || 'RFC-5424', // Options: 'RFC-3164', 'RFC-5424'
100
+ facility: process.env.SYSLOG_FACILITY || 1, // Local0
101
+ appName: process.env.SYSLOG_APP_NAME || 'MyApp',
102
+ level: 'info',
103
+ },
104
+ }
105
+ ```
106
+
107
+ ### 6. Teams
108
+ **Description:** Sends logs to a Microsoft Teams channel using an incoming webhook connector. This plugin allows you to receive log messages directly in a specified Teams channel.
109
+
110
+
111
+ ![Teams Mixed Object Plugin Output](https://github.com/NightSquawk/NightTimeLogger/blob/main/images/plugins/teams/pluginTeamsMixedOutput.png)
112
+
113
+
114
+ **Setup:**
115
+
116
+ ``` javascript
117
+ {
118
+ name: 'Teams',
119
+ config: {
120
+ webhookUrl: process.env.TEAMS_WEBHOOK_URL || "im not leaking my webhook url again",
121
+ level: process.env.LOG_LEVEL || "info",
122
+ strict: process.env.LOG_STRICT_LOGGING || true,
123
+ },
124
+ },
125
+ ```
126
+
127
+ This plugin supports additional configuration at the meta level. This allows you to create complex Teams messages with custom formatting and attachments..
128
+
129
+ ![Teams Simple Object Plugin Output](https://github.com/NightSquawk/NightTimeLogger/blob/main/images/plugins/teams/pluginTeamsSimpleOutput.png)
130
+
131
+ ```javascript
132
+ log.info('Informational message', [
133
+ { type: "location", district: "District 1", store: "Store 1", department: "Department 1"},
134
+ { type: "button", actionableBoxName: "First Button!", actionableURL: 'firstURL' },
135
+ { type: "button", actionableBoxName: "Second Button!", actionableURL: 'secondURL' },
136
+ // { type: "ping", mentionId: "example@example.com"}, // MUST BE A VALID USER EMAIL
137
+ // { type: "ping", mentionId: "example@example.com"}, // MUST BE A VALID USER EMAIL
138
+ { "type": "error", "message": "An unexpected error occurred.", "stack": "Error stack trace here" },
139
+ { "type": "metric", "name": "CPU Usage", "value": "85%" },
140
+ { "type": "metric", "name": "Request Count", "value": 123 },
141
+ ]);
142
+ ```
143
+
144
+ It also supports the complete https://adaptivecards.io/explorer/AdaptiveCard.html schema.
145
+
146
+ ![Teams Raw Object Plugin Output](https://github.com/NightSquawk/NightTimeLogger/blob/main/images/plugins/teams/pluginTeamsRawOutput.png)
147
+
148
+ ```javascript
149
+ log.error('Error message',[
150
+ { type: "raw", raw: {
151
+ body: [
152
+ { type: "TextBlock", text: "Raw Type" },
153
+ {
154
+ type: "ColumnSet",
155
+ columns: [
156
+ { type: "Column",
157
+ items: [
158
+ { "type": "TextBlock", "text": "Cats" },
159
+ { type: "Image",url: "https://png.pngtree.com/png-clipart/20230511/ourmid/pngtree-isolated-cat-on-white-background-png-image_7094927.png", altText: "Cat", wrap: true, size: "auto", separator: true, },
160
+ ]
161
+ },
162
+ { type: "Column",
163
+ items: [
164
+ { "type": "TextBlock", "text": "Dogs" },
165
+ { type: "Image",url: "https://i.pinimg.com/originals/07/b2/40/07b240561cb656aec289df602e603bee.png", altText: "Dog", wrap: true, size: "auto", separator: true, },
166
+ ]
167
+ },
168
+ { type: "Column",
169
+ items: [
170
+ { "type": "TextBlock", "text": "Birds" },
171
+ { type: "Image",url: "https://cdn.pixabay.com/photo/2016/03/02/13/59/bird-1232416_1280.png", altText: "Bird", wrap: true, size: "auto", separator: true, }
172
+ ]
173
+ },
174
+ ]
175
+ },
176
+ {
177
+ "type": "FactSet",
178
+ "facts": [
179
+ {
180
+ "title": "Fact 1",
181
+ "value": "Value 1"
182
+ },
183
+ {
184
+ "title": "Fact 2",
185
+ "value": "Value 2"
186
+ },
187
+ {
188
+ "title": "Fact 3",
189
+ "value": "Value 3"
190
+ },
191
+ {
192
+ "title": "Fact 4",
193
+ "value": "Value 5"
194
+ }
195
+ ]
196
+ },
197
+ ],
198
+ action: [
199
+ {
200
+ type: 'Action.OpenUrl',
201
+ title: "See more cats!",
202
+ url: 'https://www.google.com/search?q=cute%20cats&udm=2'
203
+ },
204
+ {
205
+ type: 'Action.OpenUrl',
206
+ title: "Chickens?",
207
+ url: 'https://www.google.com/search?q=chickens&udm=2'
208
+ },
209
+ {
210
+ "type": "Action.ShowCard",
211
+ "title": "Whats this?",
212
+ "card": {
213
+ "type": "AdaptiveCard",
214
+ "body": [
215
+ {
216
+ "type": "TextBlock",
217
+ "text": "Cool?"
218
+ }
219
+ ],
220
+ "actions": [
221
+ {
222
+ "type": "Action.Submit",
223
+ "title": "You betcha!",
224
+ "data": {
225
+ "betcha": "true"
226
+ }
227
+ }
228
+ ]
229
+ }
230
+ }
231
+ ]},
232
+ },
233
+ ]);
234
+ ```
235
+
236
+ ### Example Logger Configuration
237
+ Here is an example configuration for NightTimeLogger using multiple plugins:
238
+
239
+ ``` javascript
240
+
241
+ let config = {
242
+ level: 'internal',
243
+ file: false,
244
+ plugins: [
245
+ {
246
+ name: 'Discord',
247
+ config: {
248
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL,
249
+ avatarUrl: process.env.DISCORD_AVATAR_URL,
250
+ username: process.env.DISCORD_USERNAME || 'MyAppLogger',
251
+ level: 'info',
252
+ strict: false,
253
+ },
254
+ },
255
+ {
256
+ name: 'Sentry',
257
+ config: {
258
+ dsn: process.env.SENTRY_DSN,
259
+ release: process.env.SENTRY_RELEASE,
260
+ tracesSampleRate: 1.0,
261
+ environment: process.env.NODE_ENV || 'development',
262
+ },
263
+ },
264
+ {
265
+ name: 'MySQL',
266
+ config: {
267
+ host: process.env.MYSQL_DB_HOST,
268
+ port: process.env.MYSQL_DB_PORT || 3306,
269
+ user: process.env.MYSQL_DB_USER || 'root',
270
+ password: process.env.MYSQL_DB_PASSWORD || '',
271
+ database: process.env.MYSQL_DB_NAME || 'logs',
272
+ table: 'log_entries',
273
+ },
274
+ },
275
+ {
276
+ name: 'Jest',
277
+ },
278
+ {
279
+ name: 'Syslog',
280
+ config: {
281
+ host: process.env.SYSLOG_HOST || 'localhost',
282
+ port: process.env.SYSLOG_PORT || 514,
283
+ protocol: 'UDP',
284
+ facility: 1,
285
+ appName: 'MyAppLogger',
286
+ },
287
+ },
288
+ ],
289
+ };
290
+ ```
291
+
292
+ ### Conclusion
293
+ The NightTimeLogger is highly customizable and supports various plugins for different logging requirements. By leveraging these plugins, you can easily integrate logging into your existing infrastructure, whether it be storing logs in databases, sending them to monitoring tools like Sentry, or receiving alerts directly in Discord.
294
+
295
+ For more details on each plugin, refer to the specific plugin files in the project.
@@ -8,7 +8,7 @@ const https = require('https');
8
8
  const { URL } = require('url');
9
9
 
10
10
  const colors = require('../lib/colors');
11
- const levels = require('../lib/levels'); // Assuming this file contains the log levels mapping
11
+ const levels = require('../lib/levels');
12
12
 
13
13
  class DiscordTransport extends Transport {
14
14
  constructor(opts = {}) {
package/plugins/index.js CHANGED
@@ -10,6 +10,7 @@ const plugins = {
10
10
  Jest : require('./jest'),
11
11
  Syslog : require('./syslog'),
12
12
  Discord : require('./discord'),
13
+ Teams : require('./teams'),
13
14
  // WIP SMSMail : require('./smsMail'),
14
15
  };
15
16
 
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file /plugins/lib/syslogClient.js
3
+ * @description Custom Syslog client supporting UDP, TCP, and TLS transports with RFC-3164 and RFC-5424 formats.
4
+ */
5
+
6
+ const net = require('net');
7
+ const tls = require('tls');
8
+ const dgram = require('dgram');
9
+
10
+ class SyslogClient {
11
+ constructor(opts = {}) {
12
+ this.host = opts.host || 'localhost';
13
+ this.port = opts.port || 514;
14
+ this.protocol = opts.protocol || 'UDP'; // 'UDP', 'TCP', 'TLS'
15
+ this.rfc = opts.rfc || 'RFC-5424'; // 'RFC-3164', 'RFC-5424'
16
+ this.facility = opts.facility || 1; // Default to user-level messages
17
+ this.appName = opts.appName || 'NTLogger';
18
+ this.hostname = opts.hostname || require('os').hostname();
19
+ this.transport = null;
20
+
21
+ if (this.protocol === 'UDP') {
22
+ this.transport = dgram.createSocket('udp4');
23
+ } else if (this.protocol === 'TCP') {
24
+ this.transport = new net.Socket();
25
+ } else if (this.protocol === 'TLS') {
26
+ this.transport = new tls.TLSSocket();
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Connects to the Syslog server if using TCP or TLS.
32
+ */
33
+ connect() {
34
+ if (this.protocol === 'TCP' || this.protocol === 'TLS') {
35
+ return new Promise((resolve, reject) => {
36
+ const options = { host: this.host, port: this.port };
37
+ const connectListener = () => resolve();
38
+ const errorListener = (err) => reject(err);
39
+
40
+ if (this.protocol === 'TCP') {
41
+ this.transport.connect(options, connectListener).on('error', errorListener);
42
+ } else if (this.protocol === 'TLS') {
43
+ this.transport.connect(options, connectListener).on('error', errorListener);
44
+ }
45
+ });
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Sends a log message to the Syslog server.
51
+ * @param {string} severity - The severity level of the log.
52
+ * @param {string} message - The log message.
53
+ * @param {object} [meta] - Additional metadata for RFC-5424 structured data.
54
+ */
55
+ send(severity, message, meta = {}) {
56
+ const formattedMessage = this.formatMessage(severity, message, meta);
57
+ if (this.protocol === 'UDP') {
58
+ this.transport.send(Buffer.from(formattedMessage), 0, formattedMessage.length, this.port, this.host);
59
+ } else {
60
+ this.transport.write(formattedMessage + '\n');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Formats the Syslog message according to the selected RFC standard.
66
+ * @param {string} severity - The severity level of the log.
67
+ * @param {string} message - The log message.
68
+ * @param {object} [meta] - Additional metadata for RFC-5424 structured data.
69
+ * @returns {string} - The formatted Syslog message.
70
+ */
71
+ formatMessage(severity, message, meta) {
72
+ const timestamp = new Date().toISOString();
73
+ const priority = (this.facility * 8) + severity;
74
+
75
+ if (this.rfc === 'RFC-3164') {
76
+ return `<${priority}>${timestamp} ${this.hostname} ${this.appName}: ${message}`;
77
+ } else if (this.rfc === 'RFC-5424') {
78
+ const structuredData = this.formatStructuredData(meta.structuredData || {});
79
+ return `<${priority}>1 ${timestamp} ${this.hostname} ${this.appName} - - ${structuredData} ${message}`;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Formats the structured data for RFC-5424.
85
+ * @param {object} structuredData - The structured data to include in the log message.
86
+ * @returns {string} - The formatted structured data string.
87
+ */
88
+ formatStructuredData(structuredData) {
89
+ let sdString = '';
90
+ for (const [key, value] of Object.entries(structuredData)) {
91
+ sdString += `[${key}`;
92
+ for (const [param, val] of Object.entries(value)) {
93
+ sdString += ` ${param}="${val}"`;
94
+ }
95
+ sdString += ']';
96
+ }
97
+ return sdString || '-';
98
+ }
99
+
100
+ /**
101
+ * Closes the transport connection.
102
+ */
103
+ close() {
104
+ if (this.transport && (this.protocol === 'TCP' || this.protocol === 'TLS')) {
105
+ this.transport.end();
106
+ } else if (this.protocol === 'UDP') {
107
+ this.transport.close();
108
+ }
109
+ }
110
+ }
111
+
112
+ module.exports = SyslogClient;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * @file /plugins/teams.js
3
+ * @description Sends logs to a Teams webhook.
4
+ */
5
+
6
+ const Transport = require('winston-transport');
7
+ const https = require('https');
8
+ const { URL } = require('url');
9
+
10
+ const levels = require('../lib/levels');
11
+
12
+ class TeamsTransport extends Transport {
13
+ constructor(opts = {}) {
14
+ super(opts);
15
+
16
+ this.name = 'Teams Webhook Transport for NTLogger';
17
+
18
+ this.webhookUrl = opts.webhookUrl;
19
+ this.strict = opts.strict || false;
20
+
21
+ // Retry logic with exponential back-off
22
+ // https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL%2Ctext1#rate-limiting-for-connectors
23
+ this.maxRetries = opts.maxRetries || 3;
24
+ this.retryDelay = opts.retryDelay || 1000; // Initial delay for exponential back-off (in ms)
25
+
26
+ try {
27
+ // Set the log level
28
+ this.level = opts.level || 'info';
29
+ if (!(this.level in levels)) {
30
+ throw new Error(`Invalid log level: ${this.level}`);
31
+ }
32
+ this.levelPriority = levels[this.level]; // Get the numerical priority of the log level
33
+ } catch (error) {
34
+ console.error(`Error setting log level: ${error.message}`);
35
+ console.error(error.stack);
36
+ throw error; // Rethrow the error to stop the transport creation if level is invalid
37
+ }
38
+ }
39
+
40
+ async log(info, callback) {
41
+ setImmediate(() => {
42
+ this.emit('logged', info);
43
+ });
44
+
45
+ const { level, message, ...meta } = info;
46
+
47
+ // Check if the log level matches the configured level (strict mode) or is at or below the configured level
48
+ if (this.strict) {
49
+ if (level !== this.level) {
50
+ callback(); // Skip sending the log if it's not exactly the configured level
51
+ return;
52
+ }
53
+ } else {
54
+ if (levels[level] > this.levelPriority) {
55
+ callback(); // Skip sending the log if it's above the configured level
56
+ return;
57
+ }
58
+ }
59
+
60
+ const payload = this.createPayload(level, message, meta);
61
+ const webhookUrl = new URL(this.webhookUrl);
62
+
63
+ const options = {
64
+ hostname: webhookUrl.hostname,
65
+ path: webhookUrl.pathname + webhookUrl.search,
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ 'Content-Length': Buffer.byteLength(payload),
70
+ },
71
+ };
72
+
73
+ const sendLog = (retryCount = 0) => {
74
+ const req = https.request(options, (res) => {
75
+ let responseContent = '';
76
+
77
+ res.on('data', (chunk) => {
78
+ responseContent += chunk;
79
+ });
80
+
81
+ res.on('end', () => {
82
+ if (responseContent.includes("Microsoft Teams endpoint returned HTTP error 429")) {
83
+ console.error('Rate limit hit, retrying with exponential back-off...');
84
+ if (retryCount < this.maxRetries) {
85
+ setTimeout(() => {
86
+ sendLog(retryCount + 1);
87
+ }, this.retryDelay * Math.pow(2, retryCount)); // Exponential back-off
88
+ } else {
89
+ console.error('Max retries reached, log sending failed.');
90
+ callback();
91
+ }
92
+ } else {
93
+ callback();
94
+ }
95
+ });
96
+ });
97
+
98
+ req.on('error', (e) => {
99
+ console.error(`Failed to send log to Teams: ${e.message}`);
100
+ if (retryCount < this.maxRetries) {
101
+ console.log(`Retrying... (${retryCount + 1}/${this.maxRetries})`);
102
+ setTimeout(() => {
103
+ sendLog(retryCount + 1);
104
+ }, this.retryDelay * Math.pow(2, retryCount));
105
+ } else {
106
+ console.error('Max retries reached, log sending failed.');
107
+ callback();
108
+ }
109
+ });
110
+
111
+ req.write(payload);
112
+ req.end();
113
+ };
114
+
115
+ sendLog();
116
+ }
117
+
118
+ createPayload(level, message, meta) {
119
+ const actions = [];
120
+ const body = [
121
+ {
122
+ type: 'TextBlock',
123
+ text: `Log Level: ${level.toUpperCase()} - ${message}`,
124
+ weight: 'Bolder',
125
+ size: 'Medium'
126
+ }
127
+ ];
128
+ const mentions = [];
129
+ let mentionTextAdded = false; // Track if the mention text block has been added
130
+
131
+ // Ensure meta is treated as an array
132
+ const metaArray = Array.isArray(meta) ? meta : Object.values(meta);
133
+
134
+ metaArray.forEach(item => {
135
+ if (typeof item !== 'object' || !item.type) {
136
+ return;
137
+ }
138
+
139
+ switch (item.type) {
140
+ case 'raw':
141
+ if (item.raw && Array.isArray(item.raw.body)) {
142
+ body.push(...item.raw.body);
143
+ }
144
+ if (item.raw && Array.isArray(item.raw.action)) {
145
+ actions.push(...item.raw.action);
146
+ }
147
+ break;
148
+
149
+ case 'location':
150
+ body.push({
151
+ type: 'FactSet',
152
+ facts: [
153
+ { title: "District", value: item.district || "N/A" },
154
+ { title: "Store", value: item.store || "N/A" },
155
+ { title: "Department", value: item.department || "N/A" }
156
+ ]
157
+ });
158
+ break;
159
+
160
+ case 'button':
161
+ actions.push({
162
+ type: 'Action.OpenUrl',
163
+ title: item.actionableBoxName || "Button",
164
+ url: item.actionableURL || '#'
165
+ });
166
+ break;
167
+
168
+ case 'ping':
169
+ if (!mentionTextAdded) {
170
+ body.push({
171
+ type: 'Container',
172
+ items: [
173
+ {
174
+ type: 'TextBlock',
175
+ text: `<at>${item.mentionId}</at>`,
176
+ wrap: true
177
+ }
178
+ ]
179
+ });
180
+ mentionTextAdded = true;
181
+ }
182
+ mentions.push({
183
+ type: 'mention',
184
+ text: `<at>${item.mentionId}</at>`,
185
+ mentioned: {
186
+ id: item.mentionId
187
+ }
188
+ });
189
+ break;
190
+
191
+ case 'error':
192
+ body.push({
193
+ type: 'TextBlock',
194
+ text: `Error: ${item.message || "No message"}`,
195
+ weight: 'Bolder',
196
+ color: 'Attention',
197
+ wrap: true
198
+ });
199
+ if (item.stack) {
200
+ body.push({
201
+ type: 'TextBlock',
202
+ text: `Stack Trace: ${item.stack}`,
203
+ color: 'Attention',
204
+ wrap: true
205
+ });
206
+ }
207
+ break;
208
+
209
+ case 'metric':
210
+ body.push({
211
+ type: 'FactSet',
212
+ facts: [
213
+ { title: item.name || "Metric", value: item.value || "N/A" }
214
+ ]
215
+ });
216
+ break;
217
+
218
+ case 'ColumnSet':
219
+ if (item.complexity === 'complex') {
220
+ const columns = item.table[0].map((header, colIndex) => ({
221
+ type: 'Column',
222
+ items: [
223
+ {
224
+ type: 'TextBlock',
225
+ text: header.text || header,
226
+ weight: 'Bolder',
227
+ separator: true,
228
+ wrap: true
229
+ },
230
+ ...item.table.slice(1).map(row => {
231
+ return row[colIndex]
232
+ })
233
+ ],
234
+ width: 'auto'
235
+ }));
236
+
237
+ const columnSet = {
238
+ type: 'ColumnSet',
239
+ separator: true,
240
+ columns: columns
241
+ };
242
+
243
+ body.push(columnSet);
244
+ break;
245
+ } else {
246
+ const columns = item.table[0].map((header, colIndex) => ({
247
+ type: 'Column',
248
+ items: [
249
+ {
250
+ type: 'TextBlock',
251
+ text: header,
252
+ weight: 'Bolder',
253
+ separator: true,
254
+ wrap: true
255
+ },
256
+ ...item.table.slice(1).map(row => ({
257
+ type: 'TextBlock',
258
+ text: row[colIndex],
259
+ separator: true,
260
+ wrap: true
261
+ }))
262
+ ],
263
+ width: 'auto'
264
+ }));
265
+
266
+ const columnSet = {
267
+ type: 'ColumnSet',
268
+ separator: true,
269
+ columns: columns
270
+ };
271
+
272
+ body.push(columnSet);
273
+ break;
274
+ }
275
+ break;
276
+
277
+ default:
278
+ body.push({
279
+ type: 'TextBlock',
280
+ text: `Unsupported type: ${item.type}`,
281
+ color: 'Warning',
282
+ wrap: true
283
+ });
284
+ break;
285
+ }
286
+ });
287
+
288
+ return JSON.stringify({
289
+ type: 'message',
290
+ attachments: [
291
+ {
292
+ summary: `Log Level: ${level.toUpperCase()} - ${message}`,
293
+ contentType: 'application/vnd.microsoft.card.adaptive',
294
+ content: {
295
+ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
296
+ version: '1.2',
297
+ msteams: {
298
+ entities: mentions
299
+ },
300
+ body: body,
301
+ actions: actions
302
+ }
303
+ }
304
+ ]
305
+ });
306
+ }
307
+ }
308
+
309
+ module.exports = {
310
+ transport: TeamsTransport,
311
+ };