@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.
- package/LICENSE +26 -0
- package/README.md +126 -0
- package/index.d.ts +464 -0
- package/index.js +11 -0
- package/lib/InflightEventSet.js +53 -0
- package/lib/activation-method.js +97 -0
- package/lib/agent.js +1226 -0
- package/lib/apm-client/apm-client.js +107 -0
- package/lib/apm-client/channel-writer.js +334 -0
- package/lib/apm-client/jsonl-file-client.js +241 -0
- package/lib/apm-client/ndjson.js +20 -0
- package/lib/apm-client/noop-apm-client.js +79 -0
- package/lib/apm-client/s3-uploader.js +308 -0
- package/lib/apm-client/truncate.js +507 -0
- package/lib/async-hooks-polyfill.js +58 -0
- package/lib/cloud-metadata/aws.js +175 -0
- package/lib/cloud-metadata/azure.js +123 -0
- package/lib/cloud-metadata/callback-coordination.js +159 -0
- package/lib/cloud-metadata/gcp.js +133 -0
- package/lib/cloud-metadata/index.js +175 -0
- package/lib/config/config.js +431 -0
- package/lib/config/normalizers.js +649 -0
- package/lib/config/schema.js +946 -0
- package/lib/constants.js +35 -0
- package/lib/errors.js +303 -0
- package/lib/filters/sanitize-field-names.js +69 -0
- package/lib/http-request.js +249 -0
- package/lib/instrumentation/context.js +56 -0
- package/lib/instrumentation/dropped-spans-stats.js +112 -0
- package/lib/instrumentation/elasticsearch-shared.js +63 -0
- package/lib/instrumentation/express-utils.js +91 -0
- package/lib/instrumentation/generic-span.js +322 -0
- package/lib/instrumentation/http-shared.js +424 -0
- package/lib/instrumentation/ids.js +39 -0
- package/lib/instrumentation/index.js +1078 -0
- package/lib/instrumentation/modules/@apollo/server.js +39 -0
- package/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js +143 -0
- package/lib/instrumentation/modules/@aws-sdk/client-s3.js +230 -0
- package/lib/instrumentation/modules/@aws-sdk/client-sns.js +197 -0
- package/lib/instrumentation/modules/@aws-sdk/client-sqs.js +336 -0
- package/lib/instrumentation/modules/@elastic/elasticsearch.js +343 -0
- package/lib/instrumentation/modules/@hapi/hapi.js +221 -0
- package/lib/instrumentation/modules/@redis/client/dist/lib/client/commands-queue.js +178 -0
- package/lib/instrumentation/modules/@redis/client/dist/lib/client/index.js +49 -0
- package/lib/instrumentation/modules/@smithy/smithy-client.js +198 -0
- package/lib/instrumentation/modules/apollo-server-core.js +49 -0
- package/lib/instrumentation/modules/aws-sdk/dynamodb.js +155 -0
- package/lib/instrumentation/modules/aws-sdk/s3.js +184 -0
- package/lib/instrumentation/modules/aws-sdk/sns.js +232 -0
- package/lib/instrumentation/modules/aws-sdk/sqs.js +361 -0
- package/lib/instrumentation/modules/aws-sdk.js +76 -0
- package/lib/instrumentation/modules/bluebird.js +93 -0
- package/lib/instrumentation/modules/cassandra-driver.js +280 -0
- package/lib/instrumentation/modules/elasticsearch.js +200 -0
- package/lib/instrumentation/modules/express-graphql.js +66 -0
- package/lib/instrumentation/modules/express-queue.js +28 -0
- package/lib/instrumentation/modules/express.js +162 -0
- package/lib/instrumentation/modules/fastify.js +179 -0
- package/lib/instrumentation/modules/finalhandler.js +41 -0
- package/lib/instrumentation/modules/generic-pool.js +85 -0
- package/lib/instrumentation/modules/graphql.js +256 -0
- package/lib/instrumentation/modules/handlebars.js +33 -0
- package/lib/instrumentation/modules/http.js +112 -0
- package/lib/instrumentation/modules/http2.js +320 -0
- package/lib/instrumentation/modules/https.js +68 -0
- package/lib/instrumentation/modules/ioredis.js +94 -0
- package/lib/instrumentation/modules/jade.js +29 -0
- package/lib/instrumentation/modules/kafkajs.js +476 -0
- package/lib/instrumentation/modules/knex.js +91 -0
- package/lib/instrumentation/modules/koa-router.js +74 -0
- package/lib/instrumentation/modules/koa.js +15 -0
- package/lib/instrumentation/modules/memcached.js +100 -0
- package/lib/instrumentation/modules/mimic-response.js +45 -0
- package/lib/instrumentation/modules/mongodb/lib/cmap/connection_pool.js +40 -0
- package/lib/instrumentation/modules/mongodb-core.js +206 -0
- package/lib/instrumentation/modules/mongodb.js +259 -0
- package/lib/instrumentation/modules/mysql.js +200 -0
- package/lib/instrumentation/modules/mysql2.js +140 -0
- package/lib/instrumentation/modules/pg.js +148 -0
- package/lib/instrumentation/modules/pug.js +29 -0
- package/lib/instrumentation/modules/redis.js +176 -0
- package/lib/instrumentation/modules/restify.js +52 -0
- package/lib/instrumentation/modules/tedious.js +159 -0
- package/lib/instrumentation/modules/undici.js +270 -0
- package/lib/instrumentation/modules/ws.js +59 -0
- package/lib/instrumentation/noop-transaction.js +81 -0
- package/lib/instrumentation/run-context/AbstractRunContextManager.js +215 -0
- package/lib/instrumentation/run-context/AsyncHooksRunContextManager.js +106 -0
- package/lib/instrumentation/run-context/AsyncLocalStorageRunContextManager.js +73 -0
- package/lib/instrumentation/run-context/BasicRunContextManager.js +82 -0
- package/lib/instrumentation/run-context/RunContext.js +151 -0
- package/lib/instrumentation/run-context/index.js +23 -0
- package/lib/instrumentation/shimmer.js +123 -0
- package/lib/instrumentation/span-compression.js +239 -0
- package/lib/instrumentation/span.js +621 -0
- package/lib/instrumentation/template-shared.js +43 -0
- package/lib/instrumentation/timer.js +84 -0
- package/lib/instrumentation/transaction.js +571 -0
- package/lib/load-source-map.js +100 -0
- package/lib/logging.js +212 -0
- package/lib/metrics/index.js +92 -0
- package/lib/metrics/platforms/generic/index.js +40 -0
- package/lib/metrics/platforms/generic/process-cpu.js +22 -0
- package/lib/metrics/platforms/generic/process-top.js +157 -0
- package/lib/metrics/platforms/generic/stats.js +34 -0
- package/lib/metrics/platforms/generic/system-cpu.js +51 -0
- package/lib/metrics/platforms/linux/index.js +19 -0
- package/lib/metrics/platforms/linux/stats.js +213 -0
- package/lib/metrics/queue.js +90 -0
- package/lib/metrics/registry.js +52 -0
- package/lib/metrics/reporter.js +119 -0
- package/lib/metrics/runtime.js +77 -0
- package/lib/middleware/connect.js +16 -0
- package/lib/parsers.js +225 -0
- package/lib/propwrap.js +147 -0
- package/lib/stacktraces.js +537 -0
- package/lib/symbols.js +15 -0
- package/lib/tracecontext/index.js +115 -0
- package/lib/tracecontext/traceparent.js +185 -0
- package/lib/tracecontext/tracestate.js +388 -0
- package/lib/wildcard-matcher.js +52 -0
- package/loader.mjs +7 -0
- package/package.json +98 -0
- package/start.d.ts +8 -0
- package/start.js +29 -0
- package/types/aws-lambda.d.ts +98 -0
- 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
|
+
}
|