@redthreadlabs/tracelog 1.4.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.
Files changed (127) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +126 -0
  3. package/index.d.ts +464 -0
  4. package/index.js +11 -0
  5. package/lib/InflightEventSet.js +53 -0
  6. package/lib/activation-method.js +97 -0
  7. package/lib/agent.js +1226 -0
  8. package/lib/apm-client/apm-client.js +107 -0
  9. package/lib/apm-client/channel-writer.js +334 -0
  10. package/lib/apm-client/jsonl-file-client.js +241 -0
  11. package/lib/apm-client/ndjson.js +20 -0
  12. package/lib/apm-client/noop-apm-client.js +79 -0
  13. package/lib/apm-client/s3-uploader.js +308 -0
  14. package/lib/apm-client/truncate.js +507 -0
  15. package/lib/async-hooks-polyfill.js +58 -0
  16. package/lib/cloud-metadata/aws.js +175 -0
  17. package/lib/cloud-metadata/azure.js +123 -0
  18. package/lib/cloud-metadata/callback-coordination.js +159 -0
  19. package/lib/cloud-metadata/gcp.js +133 -0
  20. package/lib/cloud-metadata/index.js +175 -0
  21. package/lib/config/config.js +431 -0
  22. package/lib/config/normalizers.js +649 -0
  23. package/lib/config/schema.js +946 -0
  24. package/lib/constants.js +35 -0
  25. package/lib/errors.js +303 -0
  26. package/lib/filters/sanitize-field-names.js +69 -0
  27. package/lib/http-request.js +249 -0
  28. package/lib/instrumentation/context.js +56 -0
  29. package/lib/instrumentation/dropped-spans-stats.js +112 -0
  30. package/lib/instrumentation/elasticsearch-shared.js +63 -0
  31. package/lib/instrumentation/express-utils.js +91 -0
  32. package/lib/instrumentation/generic-span.js +322 -0
  33. package/lib/instrumentation/http-shared.js +424 -0
  34. package/lib/instrumentation/ids.js +39 -0
  35. package/lib/instrumentation/index.js +1078 -0
  36. package/lib/instrumentation/modules/@apollo/server.js +39 -0
  37. package/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js +143 -0
  38. package/lib/instrumentation/modules/@aws-sdk/client-s3.js +230 -0
  39. package/lib/instrumentation/modules/@aws-sdk/client-sns.js +197 -0
  40. package/lib/instrumentation/modules/@aws-sdk/client-sqs.js +336 -0
  41. package/lib/instrumentation/modules/@elastic/elasticsearch.js +343 -0
  42. package/lib/instrumentation/modules/@hapi/hapi.js +221 -0
  43. package/lib/instrumentation/modules/@redis/client/dist/lib/client/commands-queue.js +178 -0
  44. package/lib/instrumentation/modules/@redis/client/dist/lib/client/index.js +49 -0
  45. package/lib/instrumentation/modules/@smithy/smithy-client.js +198 -0
  46. package/lib/instrumentation/modules/apollo-server-core.js +49 -0
  47. package/lib/instrumentation/modules/aws-sdk/dynamodb.js +155 -0
  48. package/lib/instrumentation/modules/aws-sdk/s3.js +184 -0
  49. package/lib/instrumentation/modules/aws-sdk/sns.js +232 -0
  50. package/lib/instrumentation/modules/aws-sdk/sqs.js +361 -0
  51. package/lib/instrumentation/modules/aws-sdk.js +76 -0
  52. package/lib/instrumentation/modules/bluebird.js +93 -0
  53. package/lib/instrumentation/modules/cassandra-driver.js +280 -0
  54. package/lib/instrumentation/modules/elasticsearch.js +200 -0
  55. package/lib/instrumentation/modules/express-graphql.js +66 -0
  56. package/lib/instrumentation/modules/express-queue.js +28 -0
  57. package/lib/instrumentation/modules/express.js +162 -0
  58. package/lib/instrumentation/modules/fastify.js +179 -0
  59. package/lib/instrumentation/modules/finalhandler.js +41 -0
  60. package/lib/instrumentation/modules/generic-pool.js +85 -0
  61. package/lib/instrumentation/modules/graphql.js +256 -0
  62. package/lib/instrumentation/modules/handlebars.js +33 -0
  63. package/lib/instrumentation/modules/http.js +112 -0
  64. package/lib/instrumentation/modules/http2.js +320 -0
  65. package/lib/instrumentation/modules/https.js +68 -0
  66. package/lib/instrumentation/modules/ioredis.js +94 -0
  67. package/lib/instrumentation/modules/jade.js +29 -0
  68. package/lib/instrumentation/modules/kafkajs.js +476 -0
  69. package/lib/instrumentation/modules/knex.js +91 -0
  70. package/lib/instrumentation/modules/koa-router.js +74 -0
  71. package/lib/instrumentation/modules/koa.js +15 -0
  72. package/lib/instrumentation/modules/memcached.js +100 -0
  73. package/lib/instrumentation/modules/mimic-response.js +45 -0
  74. package/lib/instrumentation/modules/mongodb/lib/cmap/connection_pool.js +40 -0
  75. package/lib/instrumentation/modules/mongodb-core.js +206 -0
  76. package/lib/instrumentation/modules/mongodb.js +259 -0
  77. package/lib/instrumentation/modules/mysql.js +200 -0
  78. package/lib/instrumentation/modules/mysql2.js +140 -0
  79. package/lib/instrumentation/modules/pg.js +148 -0
  80. package/lib/instrumentation/modules/pug.js +29 -0
  81. package/lib/instrumentation/modules/redis.js +176 -0
  82. package/lib/instrumentation/modules/restify.js +52 -0
  83. package/lib/instrumentation/modules/tedious.js +159 -0
  84. package/lib/instrumentation/modules/undici.js +270 -0
  85. package/lib/instrumentation/modules/ws.js +59 -0
  86. package/lib/instrumentation/noop-transaction.js +81 -0
  87. package/lib/instrumentation/run-context/AbstractRunContextManager.js +215 -0
  88. package/lib/instrumentation/run-context/AsyncHooksRunContextManager.js +106 -0
  89. package/lib/instrumentation/run-context/AsyncLocalStorageRunContextManager.js +73 -0
  90. package/lib/instrumentation/run-context/BasicRunContextManager.js +82 -0
  91. package/lib/instrumentation/run-context/RunContext.js +151 -0
  92. package/lib/instrumentation/run-context/index.js +23 -0
  93. package/lib/instrumentation/shimmer.js +123 -0
  94. package/lib/instrumentation/span-compression.js +239 -0
  95. package/lib/instrumentation/span.js +621 -0
  96. package/lib/instrumentation/template-shared.js +43 -0
  97. package/lib/instrumentation/timer.js +84 -0
  98. package/lib/instrumentation/transaction.js +571 -0
  99. package/lib/load-source-map.js +100 -0
  100. package/lib/logging.js +212 -0
  101. package/lib/metrics/index.js +92 -0
  102. package/lib/metrics/platforms/generic/index.js +40 -0
  103. package/lib/metrics/platforms/generic/process-cpu.js +22 -0
  104. package/lib/metrics/platforms/generic/process-top.js +157 -0
  105. package/lib/metrics/platforms/generic/stats.js +34 -0
  106. package/lib/metrics/platforms/generic/system-cpu.js +51 -0
  107. package/lib/metrics/platforms/linux/index.js +19 -0
  108. package/lib/metrics/platforms/linux/stats.js +213 -0
  109. package/lib/metrics/queue.js +90 -0
  110. package/lib/metrics/registry.js +52 -0
  111. package/lib/metrics/reporter.js +119 -0
  112. package/lib/metrics/runtime.js +77 -0
  113. package/lib/middleware/connect.js +16 -0
  114. package/lib/parsers.js +225 -0
  115. package/lib/propwrap.js +147 -0
  116. package/lib/stacktraces.js +537 -0
  117. package/lib/symbols.js +15 -0
  118. package/lib/tracecontext/index.js +115 -0
  119. package/lib/tracecontext/traceparent.js +185 -0
  120. package/lib/tracecontext/tracestate.js +388 -0
  121. package/lib/wildcard-matcher.js +52 -0
  122. package/loader.mjs +7 -0
  123. package/package.json +98 -0
  124. package/start.d.ts +8 -0
  125. package/start.js +29 -0
  126. package/types/aws-lambda.d.ts +98 -0
  127. package/types/connect.d.ts +23 -0
@@ -0,0 +1,107 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and other contributors where applicable.
3
+ * Copyright Shaxpir Inc. All rights reserved.
4
+ * Licensed under the BSD 2-Clause License; you may not use this file except in
5
+ * compliance with the BSD 2-Clause License.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { INTAKE_STRING_MAX_SIZE } = require('../constants');
11
+ const { CloudMetadata } = require('../cloud-metadata');
12
+ const { JsonlFileClient } = require('./jsonl-file-client');
13
+ const { NoopApmClient } = require('./noop-apm-client');
14
+ const { S3Uploader } = require('./s3-uploader');
15
+
16
+ /**
17
+ * Returns a tracelog client suited for the configuration provided.
18
+ *
19
+ * @param {Object} config The agent's configuration
20
+ * @param {Object} agent The agent instance
21
+ */
22
+ function createApmClient(config, agent) {
23
+ if (config.contextPropagationOnly) {
24
+ return new NoopApmClient();
25
+ } else if (typeof config.transport === 'function') {
26
+ return config.transport(config, agent);
27
+ }
28
+
29
+ // Create S3 uploader if a bucket is configured.
30
+ let s3Uploader = null;
31
+ if (config.s3Bucket) {
32
+ s3Uploader = new S3Uploader({
33
+ bucket: config.s3Bucket,
34
+ keyTemplate:
35
+ config.s3KeyTemplate ||
36
+ '{serviceName}/{environment}/{hostname}-{channel}-{interval}.jsonl',
37
+ region: config.s3Region,
38
+ accessKeyId: config.s3AccessKeyId,
39
+ secretAccessKey: config.s3SecretAccessKey,
40
+ sessionToken: config.s3SessionToken,
41
+ s3Client: config.s3Client, // optional: inject a mock for testing
42
+ gzipCompleted: config.s3GzipCompleted,
43
+ gzipCurrent: config.s3GzipCurrent,
44
+ serviceName: config.serviceName,
45
+ environment: config.environment,
46
+ logger: config.logger,
47
+ });
48
+ }
49
+
50
+ const client = new JsonlFileClient({
51
+ serviceName: config.serviceName,
52
+ serviceVersion: config.serviceVersion,
53
+ environment: config.environment,
54
+ globalLabels: maybePairsToObject(config.globalLabels),
55
+
56
+ // Sanitize conf
57
+ truncateKeywordsAt: INTAKE_STRING_MAX_SIZE,
58
+ truncateLongFieldsAt: config.longFieldMaxLength,
59
+
60
+ // JSONL file options
61
+ logDir: config.logDir,
62
+ logFilePrefix: config.logFilePrefix,
63
+ maxFileSize: config.logMaxFileSize,
64
+ flushIntervalMs: config.logFlushIntervalMs,
65
+ rotationSchedule: config.logRotationSchedule,
66
+ maxLocalRetentionDays: config.maxLocalRetentionDays,
67
+ maxBufferSize: config.maxBufferSize,
68
+
69
+ // S3 upload
70
+ s3Uploader,
71
+ s3UploadIntervalMs: config.s3UploadIntervalMs,
72
+
73
+ // Cloud metadata
74
+ cloudMetadataFetcher:
75
+ config.cloudProvider !== 'none'
76
+ ? new CloudMetadata(
77
+ config.cloudProvider || 'auto',
78
+ agent.logger,
79
+ config.serviceName,
80
+ )
81
+ : null,
82
+
83
+ // Logging
84
+ logger: config.logger,
85
+ });
86
+
87
+ client.on('error', (err) => {
88
+ agent.logger.error('Tracelog transport error: %s', err.stack);
89
+ });
90
+
91
+ return client;
92
+ }
93
+
94
+ function maybePairsToObject(pairs) {
95
+ return pairs ? pairsToObject(pairs) : undefined;
96
+ }
97
+
98
+ function pairsToObject(pairs) {
99
+ return pairs.reduce((object, [key, value]) => {
100
+ object[key] = value;
101
+ return object;
102
+ }, {});
103
+ }
104
+
105
+ module.exports = {
106
+ createApmClient,
107
+ };
@@ -0,0 +1,334 @@
1
+ /*
2
+ * Copyright Shaxpir Inc. All rights reserved.
3
+ * Licensed under the BSD 2-Clause License; you may not use this file except in
4
+ * compliance with the BSD 2-Clause License.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const ndjson = require('./ndjson');
13
+ const truncate = require('./truncate');
14
+
15
+ const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
16
+ const DEFAULT_MAX_BUFFER_SIZE = 10000;
17
+
18
+ const ROTATION_SCHEDULES = {
19
+ hourly: 60 * 60 * 1000,
20
+ daily: 24 * 60 * 60 * 1000,
21
+ };
22
+
23
+ /**
24
+ * A ChannelWriter manages a single JSONL file stream for a named channel.
25
+ * It handles buffering, rotation (time-based and size-based), metadata
26
+ * writing, and S3 upload coordination.
27
+ */
28
+ class ChannelWriter {
29
+ /**
30
+ * @param {Object} opts
31
+ * @param {string} opts.channel - Channel name (e.g. 'server', 'client')
32
+ * @param {string} opts.baseDir - Output directory
33
+ * @param {string} opts.baseName - File prefix (e.g. 'tracelog')
34
+ * @param {Object} opts.truncOpts - Truncation options
35
+ * @param {Object} opts.metadata - Metadata object to write at start of each file
36
+ * @param {Object} [opts.metadataFilters] - Metadata filter chain
37
+ * @param {Object} [opts.s3Uploader] - S3 uploader instance
38
+ * @param {number} [opts.maxFileSize] - Max file size before rotation
39
+ * @param {number} [opts.maxBufferSize] - Max buffered lines before dropping oldest
40
+ * @param {string|number} [opts.rotationSchedule] - 'daily', 'hourly', or ms
41
+ * @param {number} [opts.maxLocalRetentionDays] - Auto-delete old files after N days
42
+ * @param {Function} [opts.clock] - Clock provider for testability
43
+ * @param {Object} [opts.logger] - Logger instance
44
+ */
45
+ constructor(opts) {
46
+ this._channel = opts.channel;
47
+ this._baseDir = opts.baseDir;
48
+ this._baseName = `${opts.baseName}-${opts.channel}`;
49
+ this._ext = '.jsonl';
50
+
51
+ this._maxFileSize = opts.maxFileSize || DEFAULT_MAX_FILE_SIZE;
52
+ this._maxBufferSize = opts.maxBufferSize || DEFAULT_MAX_BUFFER_SIZE;
53
+ this._maxLocalRetentionDays = opts.maxLocalRetentionDays || 0;
54
+ this._truncOpts = opts.truncOpts;
55
+ this._metadata = opts.metadata;
56
+ this._metadataFilters = opts.metadataFilters;
57
+ this._extraMetadata = opts.extraMetadata || null;
58
+ this._s3Uploader = opts.s3Uploader || null;
59
+ this._clock = opts.clock || (() => new Date());
60
+ this._log = opts.logger || null;
61
+
62
+ // Rotation schedule
63
+ const schedule = opts.rotationSchedule || 'daily';
64
+ if (typeof schedule === 'string' && ROTATION_SCHEDULES[schedule]) {
65
+ this._rotationIntervalMs = ROTATION_SCHEDULES[schedule];
66
+ } else if (typeof schedule === 'number' && schedule > 0) {
67
+ this._rotationIntervalMs = schedule;
68
+ } else {
69
+ this._rotationIntervalMs = ROTATION_SCHEDULES.daily;
70
+ }
71
+
72
+ this._buffer = [];
73
+ this._currentFileSize = 0;
74
+ this._wroteMetadata = false;
75
+ this._destroyed = false;
76
+
77
+ // Track current time period and sequence number for file naming.
78
+ this._currentPeriodLabel = this._getPeriodLabel(this._clock());
79
+ this._currentSeqNum = 0;
80
+ this._currentFilePath = this._buildFilePath(
81
+ this._currentPeriodLabel,
82
+ this._currentSeqNum,
83
+ );
84
+
85
+ // Resume existing file if present.
86
+ this._resumeExistingFile();
87
+
88
+ // Upload any orphaned files from previous runs.
89
+ if (this._s3Uploader) {
90
+ this._uploadOrphanedFiles();
91
+ }
92
+ }
93
+
94
+ get channel() {
95
+ return this._channel;
96
+ }
97
+
98
+ get currentFilePath() {
99
+ return this._currentFilePath;
100
+ }
101
+
102
+ send(type, data, cb) {
103
+ if (this._destroyed) {
104
+ if (cb) process.nextTick(cb);
105
+ return;
106
+ }
107
+
108
+ try {
109
+ const truncated = truncate[type]
110
+ ? truncate[type](data, this._truncOpts)
111
+ : data;
112
+
113
+ const obj = {};
114
+ obj[type] = truncated;
115
+ const line = ndjson.serialize(obj);
116
+
117
+ if (this._buffer.length >= this._maxBufferSize) {
118
+ this._buffer.shift();
119
+ }
120
+ this._buffer.push(line);
121
+ } catch (err) {
122
+ if (this._log) {
123
+ this._log.error('ChannelWriter[%s] serialize error: %s', this._channel, err.message);
124
+ }
125
+ }
126
+
127
+ if (cb) {
128
+ process.nextTick(cb);
129
+ }
130
+ }
131
+
132
+ flush() {
133
+ if (this._buffer.length === 0) return;
134
+
135
+ try {
136
+ this._checkTimeRotation();
137
+
138
+ const lines = this._buffer;
139
+ this._buffer = [];
140
+
141
+ for (const line of lines) {
142
+ if (this._currentFileSize >= this._maxFileSize) {
143
+ this._rotate();
144
+ this._currentSeqNum++;
145
+ this._currentFilePath = this._buildFilePath(
146
+ this._currentPeriodLabel,
147
+ this._currentSeqNum,
148
+ );
149
+ }
150
+
151
+ let output = '';
152
+ if (!this._wroteMetadata) {
153
+ const metadataLine = this._getMetadataLine();
154
+ if (metadataLine) {
155
+ output += metadataLine;
156
+ }
157
+ this._wroteMetadata = true;
158
+ }
159
+
160
+ output += line;
161
+ fs.appendFileSync(this._currentFilePath, output, 'utf8');
162
+ this._currentFileSize += Buffer.byteLength(output, 'utf8');
163
+ }
164
+ } catch (err) {
165
+ if (this._log) {
166
+ this._log.error('ChannelWriter[%s] flush error: %s', this._channel, err.message);
167
+ }
168
+ }
169
+ }
170
+
171
+ uploadCurrent() {
172
+ if (this._s3Uploader) {
173
+ this._s3Uploader.uploadCurrent(this._currentFilePath, { channel: this._channel });
174
+ }
175
+ }
176
+
177
+ destroy() {
178
+ if (this._destroyed) return;
179
+ this._destroyed = true;
180
+ this.flush();
181
+ this.uploadCurrent();
182
+ }
183
+
184
+ setExtraMetadata(metadata) {
185
+ this._extraMetadata = metadata;
186
+ }
187
+
188
+ // --- Internal methods ---
189
+
190
+ _getMetadataLine() {
191
+ let metadata = Object.assign({}, this._metadata);
192
+ if (this._extraMetadata) {
193
+ metadata = Object.assign(metadata, this._extraMetadata);
194
+ }
195
+ if (this._metadataFilters) {
196
+ metadata = this._metadataFilters.process(metadata);
197
+ }
198
+ if (!metadata) return null;
199
+ metadata.channel = this._channel;
200
+ return ndjson.serialize({ metadata });
201
+ }
202
+
203
+ _checkTimeRotation() {
204
+ const now = this._clock();
205
+ const periodLabel = this._getPeriodLabel(now);
206
+ if (periodLabel !== this._currentPeriodLabel) {
207
+ this._rotate();
208
+ this._currentPeriodLabel = periodLabel;
209
+ this._currentSeqNum = 0;
210
+ this._currentFilePath = this._buildFilePath(periodLabel, 0);
211
+ }
212
+ }
213
+
214
+ _rotate() {
215
+ const completedFilePath = this._currentFilePath;
216
+ this._currentFileSize = 0;
217
+ this._wroteMetadata = false;
218
+
219
+ if (this._s3Uploader && fs.existsSync(completedFilePath)) {
220
+ this._s3Uploader.uploadCompleted(completedFilePath, { channel: this._channel, interval: this._currentPeriodLabel });
221
+ }
222
+
223
+ if (this._maxLocalRetentionDays > 0) {
224
+ this._cleanupOldFiles();
225
+ }
226
+ }
227
+
228
+ _cleanupOldFiles() {
229
+ try {
230
+ const cutoff = new Date(
231
+ this._clock().getTime() -
232
+ this._maxLocalRetentionDays * 24 * 60 * 60 * 1000,
233
+ );
234
+ const cutoffLabel = this._getPeriodLabel(cutoff);
235
+ const prefix = this._baseName + '-';
236
+ const files = fs.readdirSync(this._baseDir);
237
+
238
+ for (const file of files) {
239
+ if (!file.startsWith(prefix) || !file.endsWith(this._ext)) continue;
240
+ const filePath = path.join(this._baseDir, file);
241
+ if (filePath === this._currentFilePath) continue;
242
+
243
+ const withoutPrefix = file.slice(prefix.length);
244
+ const withoutExt = withoutPrefix.slice(0, withoutPrefix.length - this._ext.length);
245
+ const periodLabel = withoutExt.replace(/\.\d+$/, '');
246
+
247
+ if (periodLabel <= cutoffLabel) {
248
+ try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
249
+ }
250
+ }
251
+ } catch (e) { /* ignore */ }
252
+ }
253
+
254
+ _buildFilePath(periodLabel, seqNum) {
255
+ const seq = seqNum > 0 ? `.${seqNum}` : '';
256
+ return path.join(
257
+ this._baseDir,
258
+ `${this._baseName}-${periodLabel}${seq}${this._ext}`,
259
+ );
260
+ }
261
+
262
+ _getPeriodLabel(date) {
263
+ const y = date.getFullYear();
264
+ const m = _pad2(date.getMonth() + 1);
265
+ const d = _pad2(date.getDate());
266
+
267
+ if (this._rotationIntervalMs >= 24 * 60 * 60 * 1000) {
268
+ return `${y}-${m}-${d}`;
269
+ }
270
+
271
+ const h = _pad2(date.getHours());
272
+ if (this._rotationIntervalMs >= 60 * 60 * 1000) {
273
+ return `${y}-${m}-${d}T${h}`;
274
+ }
275
+
276
+ const min = date.getMinutes();
277
+ const intervalMinutes = Math.floor(this._rotationIntervalMs / (60 * 1000));
278
+ const flooredMin = Math.floor(min / intervalMinutes) * intervalMinutes;
279
+ return `${y}-${m}-${d}T${h}${_pad2(flooredMin)}`;
280
+ }
281
+
282
+ _resumeExistingFile() {
283
+ let seq = 0;
284
+ while (true) {
285
+ const candidate = this._buildFilePath(this._currentPeriodLabel, seq + 1);
286
+ if (fs.existsSync(candidate)) {
287
+ seq = seq + 1;
288
+ } else {
289
+ break;
290
+ }
291
+ }
292
+
293
+ this._currentSeqNum = seq;
294
+ this._currentFilePath = this._buildFilePath(
295
+ this._currentPeriodLabel,
296
+ this._currentSeqNum,
297
+ );
298
+
299
+ try {
300
+ const stat = fs.statSync(this._currentFilePath);
301
+ this._currentFileSize = stat.size;
302
+ this._wroteMetadata = true;
303
+ } catch (e) {
304
+ // File doesn't exist yet.
305
+ }
306
+ }
307
+
308
+ _uploadOrphanedFiles() {
309
+ try {
310
+ const prefix = this._baseName + '-';
311
+ const files = fs.readdirSync(this._baseDir);
312
+
313
+ for (const file of files) {
314
+ if (!file.startsWith(prefix) || !file.endsWith(this._ext)) continue;
315
+ const filePath = path.join(this._baseDir, file);
316
+ if (filePath === this._currentFilePath) continue;
317
+
318
+ const withoutPrefix = file.slice(prefix.length);
319
+ const withoutExt = withoutPrefix.slice(0, withoutPrefix.length - this._ext.length);
320
+ const periodLabel = withoutExt.replace(/\.\d+$/, '');
321
+
322
+ if (periodLabel < this._currentPeriodLabel) {
323
+ this._s3Uploader.uploadCompleted(filePath, { channel: this._channel, interval: periodLabel });
324
+ }
325
+ }
326
+ } catch (e) { /* ignore */ }
327
+ }
328
+ }
329
+
330
+ function _pad2(n) {
331
+ return String(n).padStart(2, '0');
332
+ }
333
+
334
+ module.exports = { ChannelWriter };
@@ -0,0 +1,241 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and other contributors where applicable.
3
+ * Copyright Shaxpir Inc. All rights reserved.
4
+ * Licensed under the BSD 2-Clause License; you may not use this file except in
5
+ * compliance with the BSD 2-Clause License.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const EventEmitter = require('events');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+
14
+ const Filters = require('object-filter-sequence');
15
+
16
+ const { ChannelWriter } = require('./channel-writer');
17
+
18
+ const DEFAULT_FLUSH_INTERVAL_MS = 1000;
19
+ const DEFAULT_ROTATION_SCHEDULE = 'daily';
20
+ const DEFAULT_MAX_LOCAL_RETENTION_DAYS = 0;
21
+ const DEFAULT_MAX_BUFFER_SIZE = 10000;
22
+
23
+ const DEFAULT_CHANNEL = 'default';
24
+
25
+ /**
26
+ * A channel-aware JSONL file client. Routes records to ChannelWriter instances
27
+ * based on channel name. Each channel gets its own file, buffer, and rotation
28
+ * lifecycle. The default channel is 'default'.
29
+ */
30
+ class JsonlFileClient extends EventEmitter {
31
+ constructor(opts) {
32
+ super();
33
+
34
+ this._baseDir = opts.logDir || process.cwd();
35
+ this._baseName = opts.logFilePrefix || 'tracelog';
36
+ this._clock = opts.clock || (() => new Date());
37
+ this._log = opts.logger || null;
38
+ this._destroyed = false;
39
+
40
+ // Ensure the output directory exists.
41
+ fs.mkdirSync(this._baseDir, { recursive: true });
42
+
43
+ // Shared config for all channel writers.
44
+ this._writerOpts = {
45
+ baseDir: this._baseDir,
46
+ baseName: this._baseName,
47
+ truncOpts: {
48
+ truncateKeywordsAt:
49
+ opts.truncateKeywordsAt != null ? opts.truncateKeywordsAt : 1024,
50
+ truncateLongFieldsAt:
51
+ opts.truncateLongFieldsAt != null ? opts.truncateLongFieldsAt : 10000,
52
+ truncateErrorMessagesAt:
53
+ opts.truncateErrorMessagesAt != null
54
+ ? opts.truncateErrorMessagesAt
55
+ : undefined,
56
+ },
57
+ metadata: {
58
+ service: {
59
+ name: opts.serviceName || 'unknown',
60
+ version: opts.serviceVersion || undefined,
61
+ environment: opts.environment || undefined,
62
+ agent: { name: 'tracelog', version: require('../../package').version },
63
+ },
64
+ process: {
65
+ pid: process.pid,
66
+ title: process.title,
67
+ argv: process.argv,
68
+ },
69
+ system: {
70
+ hostname: os.hostname(),
71
+ architecture: os.arch(),
72
+ platform: os.platform(),
73
+ },
74
+ ...(opts.globalLabels && { labels: opts.globalLabels }),
75
+ },
76
+ s3Uploader: opts.s3Uploader || null,
77
+ maxFileSize: opts.maxFileSize,
78
+ maxBufferSize: opts.maxBufferSize || DEFAULT_MAX_BUFFER_SIZE,
79
+ rotationSchedule: opts.rotationSchedule || DEFAULT_ROTATION_SCHEDULE,
80
+ maxLocalRetentionDays:
81
+ opts.maxLocalRetentionDays != null
82
+ ? opts.maxLocalRetentionDays
83
+ : DEFAULT_MAX_LOCAL_RETENTION_DAYS,
84
+ clock: this._clock,
85
+ logger: this._log,
86
+ };
87
+
88
+ this._metadataFilters = new Filters();
89
+ this._extraMetadata = null;
90
+ this._cloudMetadataReady = false;
91
+
92
+ // Channel writers, keyed by channel name.
93
+ this._writers = new Map();
94
+
95
+ // Fetch cloud metadata asynchronously if a fetcher is provided.
96
+ if (opts.cloudMetadataFetcher) {
97
+ opts.cloudMetadataFetcher.getCloudMetadata((err, cloudMetadata) => {
98
+ if (!err && cloudMetadata) {
99
+ this._writerOpts.metadata.cloud = cloudMetadata;
100
+ }
101
+ this._cloudMetadataReady = true;
102
+ });
103
+ } else {
104
+ this._cloudMetadataReady = true;
105
+ }
106
+
107
+ // Create the default channel eagerly.
108
+ this._getWriter(DEFAULT_CHANNEL);
109
+
110
+ // Start periodic flush of all channels.
111
+ const flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
112
+ this._flushTimer = setInterval(() => {
113
+ for (const writer of this._writers.values()) {
114
+ writer.flush();
115
+ }
116
+ }, flushIntervalMs);
117
+ this._flushTimer.unref();
118
+
119
+ // Start periodic S3 upload of current files for all channels.
120
+ this._s3UploadIntervalMs = opts.s3UploadIntervalMs || 0;
121
+ this._s3UploadTimer = null;
122
+ if (opts.s3Uploader && this._s3UploadIntervalMs > 0) {
123
+ this._s3UploadTimer = setInterval(() => {
124
+ for (const writer of this._writers.values()) {
125
+ writer.uploadCurrent();
126
+ }
127
+ }, this._s3UploadIntervalMs);
128
+ this._s3UploadTimer.unref();
129
+ }
130
+ }
131
+
132
+ // --- Channel management ---
133
+
134
+ /**
135
+ * Get or create a ChannelWriter for the given channel name.
136
+ */
137
+ _getWriter(channel) {
138
+ if (!this._writers.has(channel)) {
139
+ const writer = new ChannelWriter({
140
+ ...this._writerOpts,
141
+ channel,
142
+ metadataFilters: this._metadataFilters,
143
+ extraMetadata: this._extraMetadata,
144
+ });
145
+ this._writers.set(channel, writer);
146
+ }
147
+ return this._writers.get(channel);
148
+ }
149
+
150
+ // --- Public API (default channel) ---
151
+
152
+ config(opts) {}
153
+
154
+ addMetadataFilter(fn) {
155
+ this._metadataFilters.push(fn);
156
+ }
157
+
158
+ setExtraMetadata(metadata) {
159
+ this._extraMetadata = metadata;
160
+ for (const writer of this._writers.values()) {
161
+ writer.setExtraMetadata(metadata);
162
+ }
163
+ }
164
+
165
+ supportsKeepingUnsampledTransaction() {
166
+ return true;
167
+ }
168
+
169
+ lambdaStart() {}
170
+ lambdaShouldRegisterTransactions() {
171
+ return true;
172
+ }
173
+ lambdaRegisterTransaction(trans, awsRequestId) {}
174
+
175
+ sendTransaction(transaction, cb) {
176
+ this._getWriter(DEFAULT_CHANNEL).send('transaction', transaction, cb);
177
+ }
178
+
179
+ sendSpan(span, cb) {
180
+ this._getWriter(DEFAULT_CHANNEL).send('span', span, cb);
181
+ }
182
+
183
+ sendError(error, cb) {
184
+ this._getWriter(DEFAULT_CHANNEL).send('error', error, cb);
185
+ }
186
+
187
+ sendMetricSet(metricset, cb) {
188
+ this._getWriter(DEFAULT_CHANNEL).send('metricset', metricset, cb);
189
+ }
190
+
191
+ sendEvent(event, cb) {
192
+ this._getWriter(DEFAULT_CHANNEL).send('event', event, cb);
193
+ }
194
+
195
+ // --- Channel-routed API ---
196
+
197
+ sendToChannel(channel, type, data, cb) {
198
+ this._getWriter(channel).send(type, data, cb);
199
+ }
200
+
201
+ // --- Lifecycle ---
202
+
203
+ flush(opts, cb) {
204
+ if (typeof opts === 'function') {
205
+ cb = opts;
206
+ opts = {};
207
+ } else if (!opts) {
208
+ opts = {};
209
+ }
210
+
211
+ for (const writer of this._writers.values()) {
212
+ writer.flush();
213
+ }
214
+
215
+ if (cb) {
216
+ process.nextTick(cb);
217
+ }
218
+ }
219
+
220
+ destroy() {
221
+ if (this._destroyed) return;
222
+ this._destroyed = true;
223
+
224
+ if (this._flushTimer) {
225
+ clearInterval(this._flushTimer);
226
+ this._flushTimer = null;
227
+ }
228
+ if (this._s3UploadTimer) {
229
+ clearInterval(this._s3UploadTimer);
230
+ this._s3UploadTimer = null;
231
+ }
232
+
233
+ for (const writer of this._writers.values()) {
234
+ writer.destroy();
235
+ }
236
+ }
237
+ }
238
+
239
+ module.exports = {
240
+ JsonlFileClient,
241
+ };
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and other contributors where applicable.
3
+ * Licensed under the BSD 2-Clause License; you may not use this file except in
4
+ * compliance with the BSD 2-Clause License.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const stringify = require('fast-safe-stringify');
10
+
11
+ exports.serialize = function serialize(obj) {
12
+ const str = tryJSONStringify(obj) || stringify(obj);
13
+ return str + '\n';
14
+ };
15
+
16
+ function tryJSONStringify(obj) {
17
+ try {
18
+ return JSON.stringify(obj);
19
+ } catch (e) {}
20
+ }