@redthreadlabs/tracelog 1.5.0 → 1.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/README.md +5 -1
- package/index.d.ts +8 -2
- package/lib/apm-client/apm-client.js +1 -5
- package/lib/apm-client/channel-writer.js +16 -3
- package/lib/apm-client/jsonl-file-client.js +10 -8
- package/lib/apm-client/s3-uploader.js +94 -43
- package/lib/config/schema.js +9 -8
- package/lib/instrumentation/index.js +67 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,12 +19,16 @@ require('tracelog').start({
|
|
|
19
19
|
serviceName: 'my-api',
|
|
20
20
|
serviceVersion: '1.0.0',
|
|
21
21
|
logDir: '/var/log/myapp',
|
|
22
|
+
defaultChannel: 'server',
|
|
22
23
|
s3Bucket: 'my-traces',
|
|
23
24
|
s3Region: 'us-east-1',
|
|
24
|
-
s3KeyTemplate: '{serviceName}/{environment}/{date}/{hostname}-{pid}-{timestamp}.jsonl',
|
|
25
25
|
});
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
S3 keys follow a fixed layout designed for prefix scans and per-channel
|
|
29
|
+
lifecycle rules: `{channel}/{interval}/{host}[_{seq}][_current].jsonl[.gz]`
|
|
30
|
+
(see CONFIG.md).
|
|
31
|
+
|
|
28
32
|
Or use the auto-start entry point with environment variables:
|
|
29
33
|
|
|
30
34
|
```bash
|
package/index.d.ts
CHANGED
|
@@ -318,10 +318,16 @@ declare namespace apm {
|
|
|
318
318
|
maxLocalRetentionDays?: number;
|
|
319
319
|
maxBufferSize?: number;
|
|
320
320
|
|
|
321
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Name of the default channel — the first segment of local filenames
|
|
323
|
+
* and S3 keys for records not routed elsewhere. Default: 'default'.
|
|
324
|
+
*/
|
|
325
|
+
defaultChannel?: string;
|
|
326
|
+
|
|
327
|
+
// Tracelog: S3 upload. The S3 key layout is fixed:
|
|
328
|
+
// {channel}/{interval}/{host}[_{seq}][_current].jsonl[.gz]
|
|
322
329
|
s3Bucket?: string;
|
|
323
330
|
s3Region?: string;
|
|
324
|
-
s3KeyTemplate?: string;
|
|
325
331
|
s3UploadIntervalMs?: number;
|
|
326
332
|
s3GzipCompleted?: boolean;
|
|
327
333
|
s3GzipCurrent?: boolean;
|
|
@@ -31,9 +31,6 @@ function createApmClient(config, agent) {
|
|
|
31
31
|
if (config.s3Bucket) {
|
|
32
32
|
s3Uploader = new S3Uploader({
|
|
33
33
|
bucket: config.s3Bucket,
|
|
34
|
-
keyTemplate:
|
|
35
|
-
config.s3KeyTemplate ||
|
|
36
|
-
'{serviceName}/{environment}/{hostname}-{channel}-{interval}.jsonl',
|
|
37
34
|
region: config.s3Region,
|
|
38
35
|
accessKeyId: config.s3AccessKeyId,
|
|
39
36
|
secretAccessKey: config.s3SecretAccessKey,
|
|
@@ -41,8 +38,6 @@ function createApmClient(config, agent) {
|
|
|
41
38
|
s3Client: config.s3Client, // optional: inject a mock for testing
|
|
42
39
|
gzipCompleted: config.s3GzipCompleted,
|
|
43
40
|
gzipCurrent: config.s3GzipCurrent,
|
|
44
|
-
serviceName: config.serviceName,
|
|
45
|
-
environment: config.environment,
|
|
46
41
|
logger: config.logger,
|
|
47
42
|
});
|
|
48
43
|
}
|
|
@@ -60,6 +55,7 @@ function createApmClient(config, agent) {
|
|
|
60
55
|
// JSONL file options
|
|
61
56
|
logDir: config.logDir,
|
|
62
57
|
logFilePrefix: config.logFilePrefix,
|
|
58
|
+
defaultChannel: config.defaultChannel,
|
|
63
59
|
maxFileSize: config.logMaxFileSize,
|
|
64
60
|
flushIntervalMs: config.logFlushIntervalMs,
|
|
65
61
|
rotationSchedule: config.logRotationSchedule,
|
|
@@ -170,7 +170,11 @@ class ChannelWriter {
|
|
|
170
170
|
|
|
171
171
|
uploadCurrent() {
|
|
172
172
|
if (this._s3Uploader) {
|
|
173
|
-
this._s3Uploader.uploadCurrent(this._currentFilePath, {
|
|
173
|
+
this._s3Uploader.uploadCurrent(this._currentFilePath, {
|
|
174
|
+
channel: this._channel,
|
|
175
|
+
interval: this._currentPeriodLabel,
|
|
176
|
+
seq: this._currentSeqNum,
|
|
177
|
+
});
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
|
|
@@ -217,7 +221,11 @@ class ChannelWriter {
|
|
|
217
221
|
this._wroteMetadata = false;
|
|
218
222
|
|
|
219
223
|
if (this._s3Uploader && fs.existsSync(completedFilePath)) {
|
|
220
|
-
this._s3Uploader.uploadCompleted(completedFilePath, {
|
|
224
|
+
this._s3Uploader.uploadCompleted(completedFilePath, {
|
|
225
|
+
channel: this._channel,
|
|
226
|
+
interval: this._currentPeriodLabel,
|
|
227
|
+
seq: this._currentSeqNum,
|
|
228
|
+
});
|
|
221
229
|
}
|
|
222
230
|
|
|
223
231
|
if (this._maxLocalRetentionDays > 0) {
|
|
@@ -317,10 +325,15 @@ class ChannelWriter {
|
|
|
317
325
|
|
|
318
326
|
const withoutPrefix = file.slice(prefix.length);
|
|
319
327
|
const withoutExt = withoutPrefix.slice(0, withoutPrefix.length - this._ext.length);
|
|
328
|
+
const seqMatch = /\.(\d+)$/.exec(withoutExt);
|
|
320
329
|
const periodLabel = withoutExt.replace(/\.\d+$/, '');
|
|
321
330
|
|
|
322
331
|
if (periodLabel < this._currentPeriodLabel) {
|
|
323
|
-
this._s3Uploader.uploadCompleted(filePath, {
|
|
332
|
+
this._s3Uploader.uploadCompleted(filePath, {
|
|
333
|
+
channel: this._channel,
|
|
334
|
+
interval: periodLabel,
|
|
335
|
+
seq: seqMatch ? parseInt(seqMatch[1], 10) : 0,
|
|
336
|
+
});
|
|
324
337
|
}
|
|
325
338
|
}
|
|
326
339
|
} catch (e) { /* ignore */ }
|
|
@@ -20,12 +20,13 @@ const DEFAULT_ROTATION_SCHEDULE = 'daily';
|
|
|
20
20
|
const DEFAULT_MAX_LOCAL_RETENTION_DAYS = 0;
|
|
21
21
|
const DEFAULT_MAX_BUFFER_SIZE = 10000;
|
|
22
22
|
|
|
23
|
-
const DEFAULT_CHANNEL = 'default';
|
|
23
|
+
const DEFAULT_CHANNEL = 'default'; // used when opts.defaultChannel is not set
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* A channel-aware JSONL file client. Routes records to ChannelWriter instances
|
|
27
27
|
* based on channel name. Each channel gets its own file, buffer, and rotation
|
|
28
|
-
* lifecycle. The default channel is 'default'
|
|
28
|
+
* lifecycle. The default channel is named 'default' unless overridden via
|
|
29
|
+
* opts.defaultChannel.
|
|
29
30
|
*/
|
|
30
31
|
class JsonlFileClient extends EventEmitter {
|
|
31
32
|
constructor(opts) {
|
|
@@ -33,6 +34,7 @@ class JsonlFileClient extends EventEmitter {
|
|
|
33
34
|
|
|
34
35
|
this._baseDir = opts.logDir || process.cwd();
|
|
35
36
|
this._baseName = opts.logFilePrefix || 'tracelog';
|
|
37
|
+
this._defaultChannel = opts.defaultChannel || DEFAULT_CHANNEL;
|
|
36
38
|
this._clock = opts.clock || (() => new Date());
|
|
37
39
|
this._log = opts.logger || null;
|
|
38
40
|
this._destroyed = false;
|
|
@@ -105,7 +107,7 @@ class JsonlFileClient extends EventEmitter {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
// Create the default channel eagerly.
|
|
108
|
-
this._getWriter(
|
|
110
|
+
this._getWriter(this._defaultChannel);
|
|
109
111
|
|
|
110
112
|
// Start periodic flush of all channels.
|
|
111
113
|
const flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
|
|
@@ -173,23 +175,23 @@ class JsonlFileClient extends EventEmitter {
|
|
|
173
175
|
lambdaRegisterTransaction(trans, awsRequestId) {}
|
|
174
176
|
|
|
175
177
|
sendTransaction(transaction, cb) {
|
|
176
|
-
this._getWriter(
|
|
178
|
+
this._getWriter(this._defaultChannel).send('transaction', transaction, cb);
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
sendSpan(span, cb) {
|
|
180
|
-
this._getWriter(
|
|
182
|
+
this._getWriter(this._defaultChannel).send('span', span, cb);
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
sendError(error, cb) {
|
|
184
|
-
this._getWriter(
|
|
186
|
+
this._getWriter(this._defaultChannel).send('error', error, cb);
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
sendMetricSet(metricset, cb) {
|
|
188
|
-
this._getWriter(
|
|
190
|
+
this._getWriter(this._defaultChannel).send('metricset', metricset, cb);
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
sendEvent(event, cb) {
|
|
192
|
-
this._getWriter(
|
|
194
|
+
this._getWriter(this._defaultChannel).send('event', event, cb);
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
// --- Channel-routed API ---
|
|
@@ -12,31 +12,52 @@ const path = require('path');
|
|
|
12
12
|
const zlib = require('zlib');
|
|
13
13
|
const { createGzip } = require('zlib');
|
|
14
14
|
const { pipeline } = require('stream');
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
S3Client,
|
|
17
|
+
PutObjectCommand,
|
|
18
|
+
DeleteObjectCommand,
|
|
19
|
+
} = require('@aws-sdk/client-s3');
|
|
20
|
+
|
|
21
|
+
// S3 key layout is FIXED (not configurable): it is the contract between
|
|
22
|
+
// tracelog and the in-browser log viewer, which scans the bucket with
|
|
23
|
+
// prefix listings. Channel comes before interval so that prefix-scoped
|
|
24
|
+
// lifecycle rules work and a date-range scan within a channel is a single
|
|
25
|
+
// lexicographically-ordered listing:
|
|
26
|
+
//
|
|
27
|
+
// {channel}/{interval}/{host}[_{seq}][_current].jsonl[.gz]
|
|
28
|
+
//
|
|
29
|
+
// server/2026-06-11/172.31.27.225.jsonl.gz
|
|
30
|
+
// server/2026-06-11/172.31.27.225_current.jsonl.gz
|
|
31
|
+
// server/2026-06-11/172.31.27.225_1.jsonl.gz
|
|
32
|
+
//
|
|
33
|
+
// There is no serviceName segment: buckets are per-service/per-env, channel
|
|
34
|
+
// names are the top-level namespace (the default channel's name is set via
|
|
35
|
+
// the `defaultChannel` config), and the service name is recorded in every
|
|
36
|
+
// file's metadata line. The basename is underscore-delimited (hostnames
|
|
37
|
+
// cannot contain underscores): host, then a numeric size-rotation seq when
|
|
38
|
+
// > 0, then the literal 'current' for the live file. A host that died
|
|
39
|
+
// mid-interval leaves its final '_current' upload in place, interval intact.
|
|
16
40
|
|
|
17
41
|
class S3Uploader {
|
|
18
42
|
/**
|
|
19
43
|
* @param {Object} opts
|
|
20
44
|
* @param {string} opts.bucket - S3 bucket name
|
|
21
|
-
* @param {string} opts.keyTemplate - S3 key template with {variable} placeholders
|
|
22
45
|
* @param {string} [opts.region] - AWS region
|
|
23
46
|
* @param {string} [opts.accessKeyId] - AWS access key ID
|
|
24
47
|
* @param {string} [opts.secretAccessKey] - AWS secret access key
|
|
25
48
|
* @param {string} [opts.sessionToken] - AWS session token
|
|
26
|
-
* @param {string} opts.serviceName - Service name for template variables
|
|
27
|
-
* @param {string} [opts.environment] - Environment for template variables
|
|
28
49
|
* @param {boolean} [opts.gzipCompleted=true] - Gzip completed files before upload
|
|
29
50
|
* @param {boolean} [opts.gzipCurrent=true] - Gzip current files before upload
|
|
30
51
|
* @param {Object} [opts.logger] - Logger instance
|
|
31
52
|
* @param {Object} [opts.s3Client] - S3 client instance (must have a send() method).
|
|
32
53
|
* Defaults to a real S3Client from @aws-sdk/client-s3. Inject a mock for testing.
|
|
33
54
|
* @param {Function} [opts.clock] - Clock provider for testability. Returns a Date.
|
|
55
|
+
* @param {string} [opts.host] - Host label override (for testing). Defaults to
|
|
56
|
+
* the normalized os.hostname().
|
|
34
57
|
*/
|
|
35
58
|
constructor(opts) {
|
|
36
59
|
this._bucket = opts.bucket;
|
|
37
|
-
this.
|
|
38
|
-
this._serviceName = opts.serviceName || 'unknown';
|
|
39
|
-
this._environment = opts.environment || 'development';
|
|
60
|
+
this._host = opts.host || normalizeHost(os.hostname());
|
|
40
61
|
this._gzipCompleted = opts.gzipCompleted !== false;
|
|
41
62
|
this._gzipCurrent = opts.gzipCurrent !== false;
|
|
42
63
|
this._log = opts.logger || null;
|
|
@@ -68,9 +89,11 @@ class S3Uploader {
|
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
/**
|
|
71
|
-
* Upload a completed (rotated) file, then delete local on success.
|
|
92
|
+
* Upload a completed (rotated) file, then delete local on success. Also
|
|
93
|
+
* deletes the now-superseded '_current' snapshot object for the same
|
|
94
|
+
* interval/seq, so only hosts that died mid-interval leave one behind.
|
|
72
95
|
* @param {string} filePath - Path to the completed JSONL file
|
|
73
|
-
* @param {Object} vars -
|
|
96
|
+
* @param {Object} vars - Key variables (channel, interval, seq)
|
|
74
97
|
*/
|
|
75
98
|
uploadCompleted(filePath, vars) {
|
|
76
99
|
if (!fs.existsSync(filePath)) return;
|
|
@@ -83,7 +106,7 @@ class S3Uploader {
|
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
_uploadCompletedGzipped(filePath, vars) {
|
|
86
|
-
const key = this.
|
|
109
|
+
const key = this._buildKey(vars) + '.gz';
|
|
87
110
|
const gzPath = filePath + '.gz';
|
|
88
111
|
|
|
89
112
|
this._pendingUploads++;
|
|
@@ -116,6 +139,7 @@ class S3Uploader {
|
|
|
116
139
|
}
|
|
117
140
|
try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
|
|
118
141
|
try { fs.unlinkSync(gzPath); } catch (e) { /* ignore */ }
|
|
142
|
+
this._deleteStaleCurrent(vars);
|
|
119
143
|
})
|
|
120
144
|
.catch((uploadErr) => {
|
|
121
145
|
this._logError(
|
|
@@ -132,7 +156,7 @@ class S3Uploader {
|
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
_uploadCompletedRaw(filePath, vars) {
|
|
135
|
-
const key = this.
|
|
159
|
+
const key = this._buildKey(vars);
|
|
136
160
|
|
|
137
161
|
this._pendingUploads++;
|
|
138
162
|
|
|
@@ -151,6 +175,7 @@ class S3Uploader {
|
|
|
151
175
|
this._log.debug('Uploaded completed log to s3://%s/%s', this._bucket, key);
|
|
152
176
|
}
|
|
153
177
|
try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
|
|
178
|
+
this._deleteStaleCurrent(vars);
|
|
154
179
|
})
|
|
155
180
|
.catch((uploadErr) => {
|
|
156
181
|
this._logError(
|
|
@@ -165,17 +190,37 @@ class S3Uploader {
|
|
|
165
190
|
}
|
|
166
191
|
|
|
167
192
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* @param {string} filePath - Path to the current JSONL file
|
|
172
|
-
* @param {Function} [cb] - Callback when upload completes
|
|
193
|
+
* Delete the '_current' snapshot object superseded by a successful
|
|
194
|
+
* completed upload of the same channel/interval/seq. Best-effort:
|
|
195
|
+
* missing objects and failures are ignored.
|
|
173
196
|
*/
|
|
197
|
+
_deleteStaleCurrent(vars) {
|
|
198
|
+
let key = this._buildKey({ ...vars, current: true });
|
|
199
|
+
if (this._gzipCurrent) {
|
|
200
|
+
key += '.gz';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this._pendingUploads++;
|
|
204
|
+
this._s3
|
|
205
|
+
.send(new DeleteObjectCommand({ Bucket: this._bucket, Key: key }))
|
|
206
|
+
.then(() => {
|
|
207
|
+
if (this._log) {
|
|
208
|
+
this._log.debug('Deleted stale current log s3://%s/%s', this._bucket, key);
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
.catch(() => { /* best-effort */ })
|
|
212
|
+
.finally(() => {
|
|
213
|
+
this._pendingUploads--;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
174
217
|
/**
|
|
175
|
-
* Upload the current (incomplete) file without deletion.
|
|
176
|
-
*
|
|
218
|
+
* Upload the current (incomplete) file without deletion. The key keeps
|
|
219
|
+
* the file's real interval and carries a '_current' basename suffix, so
|
|
220
|
+
* periodic uploads overwrite the same object, and the snapshot sorts
|
|
221
|
+
* beside its finalized siblings.
|
|
177
222
|
* @param {string} filePath - Path to the current JSONL file
|
|
178
|
-
* @param {Object} vars -
|
|
223
|
+
* @param {Object} vars - Key variables (channel, interval, seq)
|
|
179
224
|
* @param {Function} [cb] - Callback when upload completes
|
|
180
225
|
*/
|
|
181
226
|
uploadCurrent(filePath, vars, cb) {
|
|
@@ -188,7 +233,7 @@ class S3Uploader {
|
|
|
188
233
|
return;
|
|
189
234
|
}
|
|
190
235
|
|
|
191
|
-
const currentVars = { ...vars,
|
|
236
|
+
const currentVars = { ...vars, current: true };
|
|
192
237
|
const rawBody = fs.readFileSync(filePath);
|
|
193
238
|
|
|
194
239
|
if (this._gzipCurrent) {
|
|
@@ -199,7 +244,7 @@ class S3Uploader {
|
|
|
199
244
|
}
|
|
200
245
|
|
|
201
246
|
_uploadCurrentGzipped(filePath, rawBody, vars, cb) {
|
|
202
|
-
const key = this.
|
|
247
|
+
const key = this._buildKey(vars) + '.gz';
|
|
203
248
|
|
|
204
249
|
this._pendingUploads++;
|
|
205
250
|
|
|
@@ -240,7 +285,7 @@ class S3Uploader {
|
|
|
240
285
|
}
|
|
241
286
|
|
|
242
287
|
_uploadCurrentRaw(filePath, rawBody, vars, cb) {
|
|
243
|
-
const key = this.
|
|
288
|
+
const key = this._buildKey(vars);
|
|
244
289
|
|
|
245
290
|
this._pendingUploads++;
|
|
246
291
|
|
|
@@ -271,23 +316,23 @@ class S3Uploader {
|
|
|
271
316
|
}
|
|
272
317
|
|
|
273
318
|
/**
|
|
274
|
-
*
|
|
319
|
+
* Build the S3 key for a log file (see the layout contract above).
|
|
275
320
|
*
|
|
276
|
-
* @param {Object} vars -
|
|
321
|
+
* @param {Object} vars - Key variables:
|
|
277
322
|
* - {string} channel - Channel name (e.g. 'server', 'client')
|
|
278
|
-
* - {string} interval - Period label (e.g. '2026-03-17')
|
|
323
|
+
* - {string} interval - Period label (e.g. '2026-03-17')
|
|
324
|
+
* - {number} [seq] - Size-rotation sequence number within the interval
|
|
325
|
+
* - {boolean} [current] - True for the live (incomplete) file snapshot
|
|
279
326
|
*/
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return
|
|
289
|
-
return allVars[name] !== undefined ? allVars[name] : match;
|
|
290
|
-
});
|
|
327
|
+
_buildKey(vars) {
|
|
328
|
+
let basename = this._host;
|
|
329
|
+
if (vars.seq > 0) {
|
|
330
|
+
basename += `_${vars.seq}`;
|
|
331
|
+
}
|
|
332
|
+
if (vars.current) {
|
|
333
|
+
basename += '_current';
|
|
334
|
+
}
|
|
335
|
+
return `${vars.channel}/${vars.interval}/${basename}.jsonl`;
|
|
291
336
|
}
|
|
292
337
|
|
|
293
338
|
_logError(fmt, ...args) {
|
|
@@ -297,12 +342,18 @@ class S3Uploader {
|
|
|
297
342
|
}
|
|
298
343
|
}
|
|
299
344
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Normalize a hostname into the host label used in S3 keys. EC2 internal
|
|
347
|
+
* hostnames (ip-A-B-C-D or ip-A-B-C-D.ec2.internal etc.) become the dotted
|
|
348
|
+
* IP address, which avoids embedding hyphens in the basename; any other
|
|
349
|
+
* hostname is used as-is.
|
|
350
|
+
*/
|
|
351
|
+
function normalizeHost(hostname) {
|
|
352
|
+
const m = /^ip-(\d{1,3})-(\d{1,3})-(\d{1,3})-(\d{1,3})(\..*)?$/.exec(hostname);
|
|
353
|
+
if (m) {
|
|
354
|
+
return `${m[1]}.${m[2]}.${m[3]}.${m[4]}`;
|
|
355
|
+
}
|
|
356
|
+
return hostname;
|
|
306
357
|
}
|
|
307
358
|
|
|
308
|
-
module.exports = { S3Uploader };
|
|
359
|
+
module.exports = { S3Uploader, normalizeHost };
|
package/lib/config/schema.js
CHANGED
|
@@ -509,7 +509,15 @@ const CONFIG_SCHEMA = [
|
|
|
509
509
|
defaultValue: 10000,
|
|
510
510
|
envVar: 'TRACELOG_MAX_BUFFER_SIZE',
|
|
511
511
|
},
|
|
512
|
-
//
|
|
512
|
+
// Channels
|
|
513
|
+
{
|
|
514
|
+
name: 'defaultChannel',
|
|
515
|
+
configType: 'string',
|
|
516
|
+
defaultValue: 'default',
|
|
517
|
+
envVar: 'TRACELOG_DEFAULT_CHANNEL',
|
|
518
|
+
},
|
|
519
|
+
// S3 upload. The S3 key layout is fixed (the contract with the log
|
|
520
|
+
// viewer): {channel}/{interval}/{host}[_{seq}][_current].jsonl[.gz]
|
|
513
521
|
{
|
|
514
522
|
name: 's3Bucket',
|
|
515
523
|
configType: 'string',
|
|
@@ -522,13 +530,6 @@ const CONFIG_SCHEMA = [
|
|
|
522
530
|
defaultValue: undefined,
|
|
523
531
|
envVar: 'TRACELOG_S3_REGION',
|
|
524
532
|
},
|
|
525
|
-
{
|
|
526
|
-
name: 's3KeyTemplate',
|
|
527
|
-
configType: 'string',
|
|
528
|
-
defaultValue:
|
|
529
|
-
'{serviceName}/{environment}/{date}/{hostname}-{pid}-{timestamp}.jsonl',
|
|
530
|
-
envVar: 'TRACELOG_S3_KEY_TEMPLATE',
|
|
531
|
-
},
|
|
532
533
|
{
|
|
533
534
|
name: 's3UploadIntervalMs',
|
|
534
535
|
configType: 'number',
|
|
@@ -710,6 +710,14 @@ Instrumentation.prototype.addEndedTransaction = function (transaction) {
|
|
|
710
710
|
trans: transaction.id,
|
|
711
711
|
trace: transaction.traceId,
|
|
712
712
|
});
|
|
713
|
+
// The transaction's name is final now, so held spans can be routed
|
|
714
|
+
// even though the transaction record itself was dropped.
|
|
715
|
+
this._drainPendingChannelSpans(
|
|
716
|
+
transaction,
|
|
717
|
+
agent._channelForTransactionName(
|
|
718
|
+
transaction._customName || transaction._defaultName,
|
|
719
|
+
),
|
|
720
|
+
);
|
|
713
721
|
return;
|
|
714
722
|
}
|
|
715
723
|
|
|
@@ -718,6 +726,9 @@ Instrumentation.prototype.addEndedTransaction = function (transaction) {
|
|
|
718
726
|
trace: transaction.traceId,
|
|
719
727
|
});
|
|
720
728
|
const channel = agent._channelForTransactionName(payload.name);
|
|
729
|
+
// Spans held while this transaction's name was unresolved follow the
|
|
730
|
+
// same routing decision as the transaction itself.
|
|
731
|
+
this._drainPendingChannelSpans(transaction, channel);
|
|
721
732
|
if (channel && typeof agent._apmClient.sendToChannel === 'function') {
|
|
722
733
|
agent._apmClient.sendToChannel(channel, 'transaction', payload);
|
|
723
734
|
} else {
|
|
@@ -725,6 +736,31 @@ Instrumentation.prototype.addEndedTransaction = function (transaction) {
|
|
|
725
736
|
}
|
|
726
737
|
};
|
|
727
738
|
|
|
739
|
+
// Send any span payloads that were held on the transaction because the
|
|
740
|
+
// transaction's name (and so its channel routing) was not yet resolved
|
|
741
|
+
// when the span ended. See `_encodeAndSendSpan`.
|
|
742
|
+
Instrumentation.prototype._drainPendingChannelSpans = function (
|
|
743
|
+
transaction,
|
|
744
|
+
channel,
|
|
745
|
+
) {
|
|
746
|
+
const agent = this._agent;
|
|
747
|
+
const pending = transaction._pendingChannelSpans;
|
|
748
|
+
if (!pending) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
transaction._pendingChannelSpans = null;
|
|
752
|
+
if (!agent._apmClient) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
for (const payload of pending) {
|
|
756
|
+
if (channel && typeof agent._apmClient.sendToChannel === 'function') {
|
|
757
|
+
agent._apmClient.sendToChannel(channel, 'span', payload);
|
|
758
|
+
} else {
|
|
759
|
+
agent._apmClient.sendSpan(payload);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
728
764
|
Instrumentation.prototype.addEndedSpan = function (span) {
|
|
729
765
|
var agent = this._agent;
|
|
730
766
|
|
|
@@ -847,15 +883,38 @@ Instrumentation.prototype._encodeAndSendSpan = function (span) {
|
|
|
847
883
|
});
|
|
848
884
|
if (agent._apmClient) {
|
|
849
885
|
// Spans follow their transaction's channel. The transaction name
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
886
|
+
// is only meaningful for routing once *resolved* (custom or
|
|
887
|
+
// framework-set); the `name` getter must not be used here, since
|
|
888
|
+
// until the transaction ends it falls back to
|
|
889
|
+
// '<METHOD> unknown route (unnamed)', which would spuriously
|
|
890
|
+
// match rules aimed at unmatched-route traffic. When routing is
|
|
891
|
+
// configured and the name is not yet resolved (e.g. Express only
|
|
892
|
+
// names transactions when the request finishes), hold the span on
|
|
893
|
+
// its transaction; addEndedTransaction() drains held spans with
|
|
894
|
+
// the transaction's final routing decision.
|
|
895
|
+
const trans = span.transaction;
|
|
896
|
+
const routingEnabled =
|
|
897
|
+
agent._conf.transactionChannelRules &&
|
|
898
|
+
agent._conf.transactionChannelRules.length > 0 &&
|
|
899
|
+
typeof agent._apmClient.sendToChannel === 'function';
|
|
900
|
+
if (!routingEnabled) {
|
|
858
901
|
agent._apmClient.sendSpan(payload);
|
|
902
|
+
} else {
|
|
903
|
+
const resolvedName =
|
|
904
|
+
trans && (trans._customName || trans._defaultName);
|
|
905
|
+
if (!resolvedName && trans && !trans.ended) {
|
|
906
|
+
if (!trans._pendingChannelSpans) {
|
|
907
|
+
trans._pendingChannelSpans = [];
|
|
908
|
+
}
|
|
909
|
+
trans._pendingChannelSpans.push(payload);
|
|
910
|
+
} else {
|
|
911
|
+
const channel = agent._channelForTransactionName(resolvedName);
|
|
912
|
+
if (channel) {
|
|
913
|
+
agent._apmClient.sendToChannel(channel, 'span', payload);
|
|
914
|
+
} else {
|
|
915
|
+
agent._apmClient.sendSpan(payload);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
859
918
|
}
|
|
860
919
|
}
|
|
861
920
|
}
|