@redthreadlabs/tracelog 1.11.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
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',
|
|
@@ -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,7 +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.
|
|
53
|
+
"@redthreadlabs/tracelog-schema": "^0.3.0",
|
|
54
54
|
"after-all-results": "^2.0.0",
|
|
55
55
|
"async-value-promise": "^1.1.1",
|
|
56
56
|
"basic-auth": "^2.0.1",
|