@redthreadlabs/tracelog 1.10.0 → 1.11.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/lib/apm-client/s3-uploader.js +16 -142
- package/package.json +2 -1
|
@@ -18,6 +18,15 @@ const {
|
|
|
18
18
|
PutObjectCommand,
|
|
19
19
|
DeleteObjectCommand,
|
|
20
20
|
} = require('@aws-sdk/client-s3');
|
|
21
|
+
// The S3 key layout, the sidecar shape, and the histogram-deriving
|
|
22
|
+
// MetaAccumulator are the shared contract — owned by tracelog-schema so the
|
|
23
|
+
// agent (writer) and the viewer (reader) can never drift.
|
|
24
|
+
const {
|
|
25
|
+
buildKey,
|
|
26
|
+
normalizeHost,
|
|
27
|
+
MetaAccumulator,
|
|
28
|
+
sidecarKey,
|
|
29
|
+
} = require('@redthreadlabs/tracelog-schema');
|
|
21
30
|
|
|
22
31
|
// S3 key layout is FIXED (not configurable): it is the contract between
|
|
23
32
|
// tracelog and the in-browser log viewer, which scans the bucket with
|
|
@@ -47,22 +56,6 @@ const {
|
|
|
47
56
|
// not a truthful description of its contents. The viewer reads these into its
|
|
48
57
|
// size ledger for deterministic memory/cache accounting and factual rollups,
|
|
49
58
|
// and falls back to estimation for files written before sidecars existed.
|
|
50
|
-
const SIDECAR_VERSION = 1;
|
|
51
|
-
const SIDECAR_SUFFIX = '.meta.json';
|
|
52
|
-
|
|
53
|
-
function _pad2(n) {
|
|
54
|
-
return String(n).padStart(2, '0');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** epoch-ms → UTC hour-bucket label 'YYYY-MM-DDTHH' (matches the viewer). */
|
|
58
|
-
function _hourBucket(ms) {
|
|
59
|
-
const d = new Date(ms);
|
|
60
|
-
return (
|
|
61
|
-
`${d.getUTCFullYear()}-${_pad2(d.getUTCMonth() + 1)}-${_pad2(d.getUTCDate())}` +
|
|
62
|
-
`T${_pad2(d.getUTCHours())}`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
59
|
function _safeSize(p) {
|
|
67
60
|
try {
|
|
68
61
|
return fs.statSync(p).size;
|
|
@@ -71,107 +64,6 @@ function _safeSize(p) {
|
|
|
71
64
|
}
|
|
72
65
|
}
|
|
73
66
|
|
|
74
|
-
/**
|
|
75
|
-
* Derives a log file's sidecar histogram by parsing its NDJSON lines. The file
|
|
76
|
-
* on disk is the source of truth: counts come from the exact bytes being
|
|
77
|
-
* uploaded, so they cannot drift from the object, and a restart (which wipes
|
|
78
|
-
* any in-memory write-time counters) or an orphaned file from a crashed run is
|
|
79
|
-
* handled for free — we just re-derive from the file.
|
|
80
|
-
*
|
|
81
|
-
* Tolerant by design: an unparseable line is skipped (not a record); a record
|
|
82
|
-
* with a missing/garbage timestamp is counted as `malformed` rather than
|
|
83
|
-
* forced into an interval. Append-only safe: addChunk may be fed successive
|
|
84
|
-
* tails of a growing current file, since every line is newline-terminated so
|
|
85
|
-
* chunk/offset boundaries always land between lines.
|
|
86
|
-
*/
|
|
87
|
-
class MetaAccumulator {
|
|
88
|
-
constructor() {
|
|
89
|
-
this.offset = 0; // bytes consumed so far (for incremental current parsing)
|
|
90
|
-
this.records = 0;
|
|
91
|
-
this.malformed = 0;
|
|
92
|
-
this.intervals = Object.create(null); // { 'YYYY-MM-DDTHH': { kind: count } }
|
|
93
|
-
this._partial = '';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
addChunk(text) {
|
|
97
|
-
if (!text) return;
|
|
98
|
-
const s = this._partial + text;
|
|
99
|
-
let start = 0;
|
|
100
|
-
let nl;
|
|
101
|
-
while ((nl = s.indexOf('\n', start)) !== -1) {
|
|
102
|
-
this._addLine(s.slice(start, nl));
|
|
103
|
-
start = nl + 1;
|
|
104
|
-
}
|
|
105
|
-
this._partial = s.slice(start);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
flushPartial() {
|
|
109
|
-
if (this._partial) {
|
|
110
|
-
this._addLine(this._partial);
|
|
111
|
-
this._partial = '';
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
_addLine(line) {
|
|
116
|
-
const t = line.trim();
|
|
117
|
-
if (!t) return;
|
|
118
|
-
let obj;
|
|
119
|
-
try {
|
|
120
|
-
obj = JSON.parse(t);
|
|
121
|
-
} catch (e) {
|
|
122
|
-
return; // corrupt line — not a countable record (the viewer skips it too)
|
|
123
|
-
}
|
|
124
|
-
if (!obj || typeof obj !== 'object') return;
|
|
125
|
-
const kind = Object.keys(obj)[0];
|
|
126
|
-
if (!kind || kind === 'metadata') return; // the file's metadata line
|
|
127
|
-
this.records++;
|
|
128
|
-
const body = obj[kind];
|
|
129
|
-
const tsUs =
|
|
130
|
-
body &&
|
|
131
|
-
typeof body.timestamp === 'number' &&
|
|
132
|
-
isFinite(body.timestamp) &&
|
|
133
|
-
body.timestamp > 0
|
|
134
|
-
? body.timestamp
|
|
135
|
-
: 0;
|
|
136
|
-
if (!tsUs) {
|
|
137
|
-
this.malformed++;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const bucket = _hourBucket(tsUs / 1000); // serialized timestamps are epoch-µs
|
|
141
|
-
const byKind =
|
|
142
|
-
this.intervals[bucket] || (this.intervals[bucket] = Object.create(null));
|
|
143
|
-
byKind[kind] = (byKind[kind] || 0) + 1;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The sidecar object for this file. records === malformed + Σ(intervals).
|
|
148
|
-
*
|
|
149
|
-
* Keys are emitted in a fixed, sorted order at every level — top-level fields
|
|
150
|
-
* in schema order, interval buckets and their kinds sorted lexically — so the
|
|
151
|
-
* same contents always serialize to byte-identical JSON regardless of the
|
|
152
|
-
* order records arrived in. That makes a sidecar's ETag a reliable
|
|
153
|
-
* sameness check.
|
|
154
|
-
*/
|
|
155
|
-
toMeta(interval, bytes, compressed) {
|
|
156
|
-
const intervals = Object.create(null);
|
|
157
|
-
for (const hour of Object.keys(this.intervals).sort()) {
|
|
158
|
-
const src = this.intervals[hour];
|
|
159
|
-
const sorted = Object.create(null);
|
|
160
|
-
for (const kind of Object.keys(src).sort()) sorted[kind] = src[kind];
|
|
161
|
-
intervals[hour] = sorted;
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
v: SIDECAR_VERSION,
|
|
165
|
-
interval,
|
|
166
|
-
bytes,
|
|
167
|
-
compressed,
|
|
168
|
-
records: this.records,
|
|
169
|
-
malformed: this.malformed,
|
|
170
|
-
intervals,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
67
|
class S3Uploader {
|
|
176
68
|
/**
|
|
177
69
|
* @param {Object} opts
|
|
@@ -378,7 +270,7 @@ class S3Uploader {
|
|
|
378
270
|
this._currentAccs.delete(key);
|
|
379
271
|
|
|
380
272
|
// Delete the snapshot object and its sidecar (best-effort, both).
|
|
381
|
-
for (const k of [key, key
|
|
273
|
+
for (const k of [key, sidecarKey(key)]) {
|
|
382
274
|
this._pendingUploads++;
|
|
383
275
|
this._s3
|
|
384
276
|
.send(new DeleteObjectCommand({ Bucket: this._bucket, Key: k }))
|
|
@@ -538,13 +430,13 @@ class S3Uploader {
|
|
|
538
430
|
this._s3
|
|
539
431
|
.send(new PutObjectCommand({
|
|
540
432
|
Bucket: this._bucket,
|
|
541
|
-
Key: objectKey
|
|
433
|
+
Key: sidecarKey(objectKey),
|
|
542
434
|
Body: body,
|
|
543
435
|
ContentType: 'application/json',
|
|
544
436
|
}))
|
|
545
437
|
.then(() => {
|
|
546
438
|
if (this._log) {
|
|
547
|
-
this._log.debug('Uploaded sidecar s3://%s/%s
|
|
439
|
+
this._log.debug('Uploaded sidecar s3://%s/%s', this._bucket, sidecarKey(objectKey));
|
|
548
440
|
}
|
|
549
441
|
})
|
|
550
442
|
.catch((err) => {
|
|
@@ -567,14 +459,8 @@ class S3Uploader {
|
|
|
567
459
|
* - {boolean} [current] - True for the live (incomplete) file snapshot
|
|
568
460
|
*/
|
|
569
461
|
_buildKey(vars) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
basename += `_${vars.seq}`;
|
|
573
|
-
}
|
|
574
|
-
if (vars.current) {
|
|
575
|
-
basename += '_current';
|
|
576
|
-
}
|
|
577
|
-
return `${vars.channel}/${vars.interval}/${basename}.jsonl`;
|
|
462
|
+
// the layout is the shared contract; host comes from this uploader
|
|
463
|
+
return buildKey({ ...vars, host: this._host });
|
|
578
464
|
}
|
|
579
465
|
|
|
580
466
|
_logError(fmt, ...args) {
|
|
@@ -584,18 +470,6 @@ class S3Uploader {
|
|
|
584
470
|
}
|
|
585
471
|
}
|
|
586
472
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
* hostnames (ip-A-B-C-D or ip-A-B-C-D.ec2.internal etc.) become the dotted
|
|
590
|
-
* IP address, which avoids embedding hyphens in the basename; any other
|
|
591
|
-
* hostname is used as-is.
|
|
592
|
-
*/
|
|
593
|
-
function normalizeHost(hostname) {
|
|
594
|
-
const m = /^ip-(\d{1,3})-(\d{1,3})-(\d{1,3})-(\d{1,3})(\..*)?$/.exec(hostname);
|
|
595
|
-
if (m) {
|
|
596
|
-
return `${m[1]}.${m[2]}.${m[3]}.${m[4]}`;
|
|
597
|
-
}
|
|
598
|
-
return hostname;
|
|
599
|
-
}
|
|
600
|
-
|
|
473
|
+
// normalizeHost + MetaAccumulator are re-exported from tracelog-schema (the
|
|
474
|
+
// shared contract) so existing importers keep working.
|
|
601
475
|
module.exports = { S3Uploader, normalizeHost, MetaAccumulator };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redthreadlabs/tracelog",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Node.js APM instrumentation that writes traces to JSONL files",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
52
52
|
"@elastic/ecs-pino-format": "^1.5.0",
|
|
53
|
+
"@redthreadlabs/tracelog-schema": "^0.1.0",
|
|
53
54
|
"after-all-results": "^2.0.0",
|
|
54
55
|
"async-value-promise": "^1.1.1",
|
|
55
56
|
"basic-auth": "^2.0.1",
|