@redthreadlabs/tracelog 1.11.0 → 1.13.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 errorTags = Object.assign(
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(errorTags).length > 0) errorContext.tags = errorTags;
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.tags && typeof ctx.tags === 'object') {
1050
- const tags = {};
1051
- for (const [k, v] of Object.entries(ctx.tags)) {
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
- tags[k] = v;
1077
+ labels[k] = v;
1054
1078
  }
1055
1079
  }
1056
- if (Object.keys(tags).length > 0) result.tags = tags;
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,92 @@ 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.id) !== undefined) d.id = str(input.device.id);
1160
+ if (str(input.device.model) !== undefined) d.model = str(input.device.model);
1161
+ if (str(input.device.brand) !== undefined) d.brand = str(input.device.brand);
1162
+ if (str(input.device.type) !== undefined) d.type = str(input.device.type);
1163
+ if (typeof input.device.year_class === 'number' && isFinite(input.device.year_class)) {
1164
+ d.year_class = input.device.year_class;
1165
+ }
1166
+ if (input.device.screen && typeof input.device.screen === 'object') {
1167
+ const sc = {};
1168
+ for (const k of ['width', 'height', 'pixel_ratio']) {
1169
+ if (typeof input.device.screen[k] === 'number' && isFinite(input.device.screen[k])) {
1170
+ sc[k] = input.device.screen[k];
1171
+ }
1172
+ }
1173
+ if (Object.keys(sc).length > 0) d.screen = sc;
1174
+ }
1175
+ if (Object.keys(d).length > 0) origin.device = d;
1176
+ }
1177
+ return Object.keys(origin).length > 0 ? origin : null;
1178
+ }
1179
+
1070
1180
  function _buildEvent(type, opts) {
1071
1181
  const event = {
1072
1182
  type: type || 'custom',
@@ -508,7 +508,7 @@ Span.prototype._encode = function (cb) {
508
508
  haveContext = true;
509
509
  }
510
510
  if (self._labels) {
511
- context.tags = self._labels;
511
+ context.labels = self._labels;
512
512
  haveContext = true;
513
513
  }
514
514
  if (haveContext) {
@@ -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.tags = this._labels;
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.11.0",
3
+ "version": "1.13.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.1.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",