@redthreadlabs/tracelog 1.10.0 → 1.12.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/agent.js +116 -7
- package/lib/apm-client/s3-uploader.js +16 -142
- package/lib/instrumentation/span.js +1 -1
- package/lib/instrumentation/transaction.js +1 -1
- package/package.json +2 -1
package/lib/agent.js
CHANGED
|
@@ -650,13 +650,13 @@ Agent.prototype.writeError = function (err, opts, cb) {
|
|
|
650
650
|
opts.user,
|
|
651
651
|
);
|
|
652
652
|
if (Object.keys(errorUser).length > 0) errorContext.user = errorUser;
|
|
653
|
-
const
|
|
653
|
+
const errorLabels = Object.assign(
|
|
654
654
|
{},
|
|
655
655
|
trans && trans._labels,
|
|
656
656
|
opts.tags,
|
|
657
657
|
opts.labels,
|
|
658
658
|
);
|
|
659
|
-
if (Object.keys(
|
|
659
|
+
if (Object.keys(errorLabels).length > 0) errorContext.labels = errorLabels;
|
|
660
660
|
const errorCustom = Object.assign({}, trans && trans._custom, opts.custom);
|
|
661
661
|
if (Object.keys(errorCustom).length > 0) errorContext.custom = errorCustom;
|
|
662
662
|
if (req) {
|
|
@@ -951,6 +951,28 @@ Channel.prototype.writeSpan = function (input) {
|
|
|
951
951
|
}
|
|
952
952
|
};
|
|
953
953
|
|
|
954
|
+
// Forward client-originated event records as-is (validated + bounded). Distinct
|
|
955
|
+
// from writeEvents, which builds events from the server's own ms-based API.
|
|
956
|
+
Channel.prototype.writeClientEvents = function (events) {
|
|
957
|
+
if (!this._agent._apmClient || !Array.isArray(events)) return;
|
|
958
|
+
for (const input of events) {
|
|
959
|
+
const event = _validateClientEvent(input);
|
|
960
|
+
if (event) {
|
|
961
|
+
this._agent._apmClient.sendToChannel(this._name, 'event', event);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
// Write a client's RecordOrigin (service + environment) as a `metadata` record,
|
|
967
|
+
// keyed by its lifetime_id — the in-stream origin that records join to.
|
|
968
|
+
Channel.prototype.writeRecordOrigin = function (origin) {
|
|
969
|
+
if (!this._agent._apmClient) return;
|
|
970
|
+
const validated = _validateRecordOrigin(origin);
|
|
971
|
+
if (validated) {
|
|
972
|
+
this._agent._apmClient.sendToChannel(this._name, 'metadata', validated);
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
954
976
|
// --- Write transaction/span on the default channel ---
|
|
955
977
|
|
|
956
978
|
// Write a validated transaction record to the default channel.
|
|
@@ -1003,6 +1025,7 @@ function _validateTransaction(input) {
|
|
|
1003
1025
|
if (typeof input.duration === 'number' && isFinite(input.duration)) transaction.duration = input.duration;
|
|
1004
1026
|
if (typeof input.result === 'string') transaction.result = input.result.slice(0, 1024);
|
|
1005
1027
|
if (typeof input.parent_id === 'string' && HEX_16.test(input.parent_id)) transaction.parent_id = input.parent_id;
|
|
1028
|
+
if (typeof input.lifetime_id === 'string' && HEX_16.test(input.lifetime_id)) transaction.lifetime_id = input.lifetime_id;
|
|
1006
1029
|
if (input.context && typeof input.context === 'object') {
|
|
1007
1030
|
const ctx = _sanitizeContext(input.context);
|
|
1008
1031
|
if (ctx) transaction.context = ctx;
|
|
@@ -1033,6 +1056,7 @@ function _validateSpan(input) {
|
|
|
1033
1056
|
if (typeof input.duration === 'number' && isFinite(input.duration)) span.duration = input.duration;
|
|
1034
1057
|
if (typeof input.subtype === 'string') span.subtype = input.subtype.slice(0, 1024);
|
|
1035
1058
|
if (typeof input.action === 'string') span.action = input.action.slice(0, 1024);
|
|
1059
|
+
if (typeof input.lifetime_id === 'string' && HEX_16.test(input.lifetime_id)) span.lifetime_id = input.lifetime_id;
|
|
1036
1060
|
if (input.context && typeof input.context === 'object') {
|
|
1037
1061
|
const ctx = _sanitizeContext(input.context);
|
|
1038
1062
|
if (ctx) span.context = ctx;
|
|
@@ -1046,14 +1070,14 @@ function _validateSpan(input) {
|
|
|
1046
1070
|
function _sanitizeContext(ctx) {
|
|
1047
1071
|
const result = {};
|
|
1048
1072
|
|
|
1049
|
-
if (ctx.
|
|
1050
|
-
const
|
|
1051
|
-
for (const [k, v] of Object.entries(ctx.
|
|
1073
|
+
if (ctx.labels && typeof ctx.labels === 'object') {
|
|
1074
|
+
const labels = {};
|
|
1075
|
+
for (const [k, v] of Object.entries(ctx.labels)) {
|
|
1052
1076
|
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
1053
|
-
|
|
1077
|
+
labels[k] = v;
|
|
1054
1078
|
}
|
|
1055
1079
|
}
|
|
1056
|
-
if (Object.keys(
|
|
1080
|
+
if (Object.keys(labels).length > 0) result.labels = labels;
|
|
1057
1081
|
}
|
|
1058
1082
|
|
|
1059
1083
|
if (ctx.user && typeof ctx.user === 'object') {
|
|
@@ -1067,6 +1091,91 @@ function _sanitizeContext(ctx) {
|
|
|
1067
1091
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
1068
1092
|
}
|
|
1069
1093
|
|
|
1094
|
+
const VALID_EVENT_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
1095
|
+
|
|
1096
|
+
// Validate a client-originated event record, forwarded as-is to the channel
|
|
1097
|
+
// (the server has already stamped user/lifetime_id). Untrusted input, so every
|
|
1098
|
+
// string is length-bounded and only known fields survive. Unlike _buildEvent
|
|
1099
|
+
// (the server's own ms API) this carries the record's epoch-µs timestamp as-is.
|
|
1100
|
+
function _validateClientEvent(input) {
|
|
1101
|
+
if (!input || typeof input !== 'object') return null;
|
|
1102
|
+
const event = {
|
|
1103
|
+
type:
|
|
1104
|
+
typeof input.type === 'string' && input.type
|
|
1105
|
+
? input.type.slice(0, 256)
|
|
1106
|
+
: 'client-log',
|
|
1107
|
+
timestamp:
|
|
1108
|
+
typeof input.timestamp === 'number' && isFinite(input.timestamp)
|
|
1109
|
+
? Math.floor(input.timestamp)
|
|
1110
|
+
: Date.now() * 1000,
|
|
1111
|
+
level: VALID_EVENT_LEVELS.includes(input.level) ? input.level : 'info',
|
|
1112
|
+
};
|
|
1113
|
+
if (typeof input.message === 'string') event.message = input.message.slice(0, 10000);
|
|
1114
|
+
if (typeof input.locale === 'string' && input.locale) event.locale = input.locale.slice(0, 64);
|
|
1115
|
+
if (typeof input.tz_offset === 'number' && isFinite(input.tz_offset)) event.tz_offset = input.tz_offset;
|
|
1116
|
+
if (typeof input.lifetime_id === 'string' && HEX_16.test(input.lifetime_id)) event.lifetime_id = input.lifetime_id;
|
|
1117
|
+
if (input.context && typeof input.context === 'object') {
|
|
1118
|
+
const ctx = _sanitizeContext(input.context);
|
|
1119
|
+
if (ctx) event.context = ctx;
|
|
1120
|
+
}
|
|
1121
|
+
if (input.error && typeof input.error === 'object') {
|
|
1122
|
+
const err = {};
|
|
1123
|
+
if (typeof input.error.message === 'string') err.message = input.error.message.slice(0, 10000);
|
|
1124
|
+
if (typeof input.error.type === 'string') err.type = input.error.type.slice(0, 1024);
|
|
1125
|
+
if (typeof input.error.code === 'string' || typeof input.error.code === 'number') {
|
|
1126
|
+
err.code = String(input.error.code).slice(0, 1024);
|
|
1127
|
+
}
|
|
1128
|
+
if (typeof input.error.stack === 'string') err.stack = input.error.stack.slice(0, 10000);
|
|
1129
|
+
if (Object.keys(err).length > 0) event.error = err;
|
|
1130
|
+
}
|
|
1131
|
+
return event;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Validate a client RecordOrigin (service + environment) for a `metadata`
|
|
1135
|
+
// record. Untrusted input: bound strings, keep only known fields.
|
|
1136
|
+
function _validateRecordOrigin(input) {
|
|
1137
|
+
if (!input || typeof input !== 'object') return null;
|
|
1138
|
+
const str = (v) => (typeof v === 'string' ? v.slice(0, 256) : undefined);
|
|
1139
|
+
const nameVer = (o) => {
|
|
1140
|
+
if (!o || typeof o !== 'object') return undefined;
|
|
1141
|
+
const r = {};
|
|
1142
|
+
if (str(o.name) !== undefined) r.name = str(o.name);
|
|
1143
|
+
if (str(o.version) !== undefined) r.version = str(o.version);
|
|
1144
|
+
return Object.keys(r).length > 0 ? r : undefined;
|
|
1145
|
+
};
|
|
1146
|
+
const origin = {};
|
|
1147
|
+
if (typeof input.lifetime_id === 'string' && HEX_16.test(input.lifetime_id)) origin.lifetime_id = input.lifetime_id;
|
|
1148
|
+
const service = nameVer(input.service);
|
|
1149
|
+
if (service) origin.service = service;
|
|
1150
|
+
const runtime = nameVer(input.runtime);
|
|
1151
|
+
if (runtime) origin.runtime = runtime;
|
|
1152
|
+
const os = nameVer(input.os);
|
|
1153
|
+
if (os) origin.os = os;
|
|
1154
|
+
if (input.host && typeof input.host === 'object' && str(input.host.name) !== undefined) {
|
|
1155
|
+
origin.host = { name: str(input.host.name) };
|
|
1156
|
+
}
|
|
1157
|
+
if (input.device && typeof input.device === 'object') {
|
|
1158
|
+
const d = {};
|
|
1159
|
+
if (str(input.device.model) !== undefined) d.model = str(input.device.model);
|
|
1160
|
+
if (str(input.device.brand) !== undefined) d.brand = str(input.device.brand);
|
|
1161
|
+
if (str(input.device.type) !== undefined) d.type = str(input.device.type);
|
|
1162
|
+
if (typeof input.device.year_class === 'number' && isFinite(input.device.year_class)) {
|
|
1163
|
+
d.year_class = input.device.year_class;
|
|
1164
|
+
}
|
|
1165
|
+
if (input.device.screen && typeof input.device.screen === 'object') {
|
|
1166
|
+
const sc = {};
|
|
1167
|
+
for (const k of ['width', 'height', 'pixel_ratio']) {
|
|
1168
|
+
if (typeof input.device.screen[k] === 'number' && isFinite(input.device.screen[k])) {
|
|
1169
|
+
sc[k] = input.device.screen[k];
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (Object.keys(sc).length > 0) d.screen = sc;
|
|
1173
|
+
}
|
|
1174
|
+
if (Object.keys(d).length > 0) origin.device = d;
|
|
1175
|
+
}
|
|
1176
|
+
return Object.keys(origin).length > 0 ? origin : null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1070
1179
|
function _buildEvent(type, opts) {
|
|
1071
1180
|
const event = {
|
|
1072
1181
|
type: type || 'custom',
|
|
@@ -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 };
|
|
@@ -298,7 +298,7 @@ Transaction.prototype.toJSON = function () {
|
|
|
298
298
|
);
|
|
299
299
|
if (Object.keys(user).length > 0) context.user = user;
|
|
300
300
|
if (this._labels && Object.keys(this._labels).length > 0) {
|
|
301
|
-
context.
|
|
301
|
+
context.labels = this._labels;
|
|
302
302
|
}
|
|
303
303
|
if (this._custom && Object.keys(this._custom).length > 0) {
|
|
304
304
|
context.custom = this._custom;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redthreadlabs/tracelog",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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.3.0",
|
|
53
54
|
"after-all-results": "^2.0.0",
|
|
54
55
|
"async-value-promise": "^1.1.1",
|
|
55
56
|
"basic-auth": "^2.0.1",
|