@redthreadlabs/tracelog 1.6.0 → 1.7.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 +105 -42
- package/lib/apm-client/apm-client.js +1 -0
- package/lib/apm-client/channel-writer.js +23 -2
- package/lib/apm-client/jsonl-file-client.js +13 -1
- package/lib/errors.js +1 -1
- package/lib/instrumentation/transaction.js +43 -14
- package/lib/metrics/registry.js +3 -1
- package/package.json +1 -1
package/lib/agent.js
CHANGED
|
@@ -640,17 +640,25 @@ Agent.prototype.writeError = function (err, opts, cb) {
|
|
|
640
640
|
// for a response that hasn't yet completed (no headers, unset status_code,
|
|
641
641
|
// etc.).
|
|
642
642
|
setImmediate(() => {
|
|
643
|
-
// Gather `error.context.*`.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
643
|
+
// Gather `error.context.*`. Members without content are omitted
|
|
644
|
+
// entirely rather than serialized as empty `{}` placeholders.
|
|
645
|
+
const errorContext = {};
|
|
646
|
+
const errorUser = Object.assign(
|
|
647
|
+
{},
|
|
648
|
+
req && parsers.getUserContextFromRequest(req),
|
|
649
|
+
trans && trans._user,
|
|
650
|
+
opts.user,
|
|
651
|
+
);
|
|
652
|
+
if (Object.keys(errorUser).length > 0) errorContext.user = errorUser;
|
|
653
|
+
const errorTags = Object.assign(
|
|
654
|
+
{},
|
|
655
|
+
trans && trans._labels,
|
|
656
|
+
opts.tags,
|
|
657
|
+
opts.labels,
|
|
658
|
+
);
|
|
659
|
+
if (Object.keys(errorTags).length > 0) errorContext.tags = errorTags;
|
|
660
|
+
const errorCustom = Object.assign({}, trans && trans._custom, opts.custom);
|
|
661
|
+
if (Object.keys(errorCustom).length > 0) errorContext.custom = errorCustom;
|
|
654
662
|
if (req) {
|
|
655
663
|
errorContext.request = parsers.getContextFromRequest(
|
|
656
664
|
req,
|
|
@@ -733,10 +741,11 @@ Agent.prototype.writeError = function (err, opts, cb) {
|
|
|
733
741
|
// - `message` {string} - Human-readable description.
|
|
734
742
|
// - `level` {string} - Severity: 'debug', 'info', 'warn', 'error', 'fatal'.
|
|
735
743
|
// - `timestamp` {number} - Unix epoch ms. Defaults to Date.now().
|
|
744
|
+
// (Serialized as epoch-µs, like every other record kind.)
|
|
736
745
|
// - `duration` {number} - Duration in ms.
|
|
737
|
-
// - `error` {Error|
|
|
738
|
-
//
|
|
739
|
-
//
|
|
746
|
+
// - `error` {Error|string|Object} - Error to attach. Safely extracts
|
|
747
|
+
// message, type, code, and stack into `event.error`, including from
|
|
748
|
+
// error-like plain objects.
|
|
740
749
|
// - `user` {Object} - End-user identity: { id, email, username }.
|
|
741
750
|
// - `client` {Object} - Client environment: { name, version, os, device, runtime }.
|
|
742
751
|
// - `params` {Object} - Arbitrary key-value data.
|
|
@@ -764,15 +773,19 @@ Agent.prototype.writeEvent = function (type, opts, cb) {
|
|
|
764
773
|
}
|
|
765
774
|
|
|
766
775
|
if (this._apmClient) {
|
|
767
|
-
const event =
|
|
768
|
-
|
|
769
|
-
|
|
776
|
+
const event = _buildEvent(type, opts);
|
|
777
|
+
// Correlate with the active trace, when there is one. Batch writes
|
|
778
|
+
// (writeEvents) intentionally do not do this: their events originate
|
|
779
|
+
// on remote clients, not in the transaction that relayed them.
|
|
780
|
+
const trans = this.currentTransaction;
|
|
781
|
+
if (trans) {
|
|
782
|
+
event.trace_id = trans.traceId;
|
|
783
|
+
event.transaction_id = trans.id;
|
|
784
|
+
}
|
|
785
|
+
const filtered = this._eventFilters.process(event);
|
|
786
|
+
if (filtered) {
|
|
787
|
+
this._apmClient.sendEvent(filtered);
|
|
770
788
|
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// If an error was provided, also emit a full error record
|
|
774
|
-
if (opts.error != null) {
|
|
775
|
-
this.writeError(opts.error);
|
|
776
789
|
}
|
|
777
790
|
|
|
778
791
|
if (cb) {
|
|
@@ -881,13 +894,16 @@ Channel.prototype.writeEvent = function (type, opts, cb) {
|
|
|
881
894
|
return;
|
|
882
895
|
}
|
|
883
896
|
|
|
884
|
-
const event =
|
|
885
|
-
|
|
886
|
-
|
|
897
|
+
const event = _buildEvent(type, opts);
|
|
898
|
+
// Correlate with the active trace, when there is one (see writeEvent).
|
|
899
|
+
const trans = this._agent.currentTransaction;
|
|
900
|
+
if (trans) {
|
|
901
|
+
event.trace_id = trans.traceId;
|
|
902
|
+
event.transaction_id = trans.id;
|
|
887
903
|
}
|
|
888
|
-
|
|
889
|
-
if (
|
|
890
|
-
this._agent.
|
|
904
|
+
const filtered = this._agent._eventFilters.process(event);
|
|
905
|
+
if (filtered) {
|
|
906
|
+
this._agent._apmClient.sendToChannel(this._name, 'event', filtered);
|
|
891
907
|
}
|
|
892
908
|
|
|
893
909
|
if (cb) process.nextTick(cb);
|
|
@@ -1054,31 +1070,78 @@ function _sanitizeContext(ctx) {
|
|
|
1054
1070
|
function _buildEvent(type, opts) {
|
|
1055
1071
|
const event = {
|
|
1056
1072
|
type: type || 'custom',
|
|
1057
|
-
|
|
1073
|
+
// The API accepts epoch-ms (JS-native, e.g. Date.now()); the record
|
|
1074
|
+
// serializes epoch-µs so every record kind shares one timestamp unit.
|
|
1075
|
+
timestamp:
|
|
1076
|
+
(typeof opts.timestamp === 'number' && isFinite(opts.timestamp)
|
|
1077
|
+
? opts.timestamp
|
|
1078
|
+
: Date.now()) * 1000,
|
|
1079
|
+
level:
|
|
1080
|
+
typeof opts.level === 'string' && opts.level !== ''
|
|
1081
|
+
? opts.level
|
|
1082
|
+
: 'info',
|
|
1058
1083
|
};
|
|
1059
1084
|
if (opts.message != null) event.message = opts.message;
|
|
1060
|
-
if (opts.level != null) event.level = opts.level;
|
|
1061
1085
|
if (opts.duration != null) event.duration = opts.duration;
|
|
1062
1086
|
if (opts.user != null) event.user = opts.user;
|
|
1063
1087
|
if (opts.client != null) event.client = opts.client;
|
|
1064
1088
|
if (opts.params != null) event.params = opts.params;
|
|
1065
1089
|
if (opts.error != null) {
|
|
1066
|
-
|
|
1067
|
-
event.error = {};
|
|
1068
|
-
if (isError(err)) {
|
|
1069
|
-
event.error.message = String(err.message || '');
|
|
1070
|
-
if (err.name) event.error.type = String(err.name);
|
|
1071
|
-
if (err.code) event.error.code = String(err.code);
|
|
1072
|
-
if (err.stack) event.error.stack = String(err.stack);
|
|
1073
|
-
} else if (typeof err === 'string') {
|
|
1074
|
-
event.error.message = err;
|
|
1075
|
-
} else {
|
|
1076
|
-
event.error.message = String(err);
|
|
1077
|
-
}
|
|
1090
|
+
event.error = _extractEventError(opts.error);
|
|
1078
1091
|
}
|
|
1079
1092
|
return event;
|
|
1080
1093
|
}
|
|
1081
1094
|
|
|
1095
|
+
// Build the `event.error` sub-object from an Error instance, a string, or
|
|
1096
|
+
// an error-like plain object. The plain-object case matters: errors that
|
|
1097
|
+
// crossed a process boundary (e.g. client errors relayed through a server
|
|
1098
|
+
// endpoint, ShareDB op errors) arrive as `{message, code, ...}` objects,
|
|
1099
|
+
// and stringifying those yields the useless '[object Object]'.
|
|
1100
|
+
function _extractEventError(err) {
|
|
1101
|
+
const out = {};
|
|
1102
|
+
if (isError(err)) {
|
|
1103
|
+
out.message = String(err.message || '');
|
|
1104
|
+
if (err.name) out.type = String(err.name);
|
|
1105
|
+
if (err.code != null) out.code = String(err.code);
|
|
1106
|
+
if (err.stack) out.stack = String(err.stack);
|
|
1107
|
+
} else if (typeof err === 'string') {
|
|
1108
|
+
out.message = err;
|
|
1109
|
+
} else if (err && typeof err === 'object') {
|
|
1110
|
+
out.message =
|
|
1111
|
+
typeof err.message === 'string' && err.message !== ''
|
|
1112
|
+
? err.message
|
|
1113
|
+
: _safeJson(err);
|
|
1114
|
+
if (err.name != null || err.type != null) {
|
|
1115
|
+
out.type = String(err.name != null ? err.name : err.type);
|
|
1116
|
+
}
|
|
1117
|
+
if (err.code != null) out.code = String(err.code);
|
|
1118
|
+
if (typeof err.stack === 'string' && err.stack !== '') {
|
|
1119
|
+
out.stack = err.stack;
|
|
1120
|
+
}
|
|
1121
|
+
} else {
|
|
1122
|
+
out.message = String(err);
|
|
1123
|
+
}
|
|
1124
|
+
return out;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Bounded JSON representation for error-like objects without a usable
|
|
1128
|
+
// `message`. Falls back to a key listing when JSON.stringify fails or
|
|
1129
|
+
// produces nothing informative.
|
|
1130
|
+
function _safeJson(obj) {
|
|
1131
|
+
try {
|
|
1132
|
+
const json = JSON.stringify(obj);
|
|
1133
|
+
if (json && json !== '{}') {
|
|
1134
|
+
return json.length > 500 ? json.slice(0, 500) + '…' : json;
|
|
1135
|
+
}
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
// fall through to the key listing
|
|
1138
|
+
}
|
|
1139
|
+
const keys = Object.keys(obj);
|
|
1140
|
+
return keys.length > 0
|
|
1141
|
+
? '[object with keys: ' + keys.join(', ') + ']'
|
|
1142
|
+
: Object.prototype.toString.call(obj);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1082
1145
|
// The optional callback will be called with the error object after the error
|
|
1083
1146
|
// have been sent to the intake API. If no callback have been provided we will
|
|
1084
1147
|
// automatically terminate the process, so if you provide a callback you must
|
|
@@ -44,6 +44,7 @@ function createApmClient(config, agent) {
|
|
|
44
44
|
|
|
45
45
|
const client = new JsonlFileClient({
|
|
46
46
|
serviceName: config.serviceName,
|
|
47
|
+
serviceNodeName: config.serviceNodeName,
|
|
47
48
|
serviceVersion: config.serviceVersion,
|
|
48
49
|
environment: config.environment,
|
|
49
50
|
globalLabels: maybePairsToObject(config.globalLabels),
|
|
@@ -55,10 +55,15 @@ class ChannelWriter {
|
|
|
55
55
|
this._metadata = opts.metadata;
|
|
56
56
|
this._metadataFilters = opts.metadataFilters;
|
|
57
57
|
this._extraMetadata = opts.extraMetadata || null;
|
|
58
|
+
this._metadataReady = opts.metadataReady || null;
|
|
58
59
|
this._s3Uploader = opts.s3Uploader || null;
|
|
59
60
|
this._clock = opts.clock || (() => new Date());
|
|
60
61
|
this._log = opts.logger || null;
|
|
61
62
|
|
|
63
|
+
// How long flush() may hold the first write waiting for asynchronously
|
|
64
|
+
// fetched metadata (e.g. cloud) before proceeding without it.
|
|
65
|
+
this._metadataWaitDeadline = this._clock().getTime() + 10 * 1000;
|
|
66
|
+
|
|
62
67
|
// Rotation schedule
|
|
63
68
|
const schedule = opts.rotationSchedule || 'daily';
|
|
64
69
|
if (typeof schedule === 'string' && ROTATION_SCHEDULES[schedule]) {
|
|
@@ -129,9 +134,23 @@ class ChannelWriter {
|
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
flush() {
|
|
137
|
+
flush(opts) {
|
|
133
138
|
if (this._buffer.length === 0) return;
|
|
134
139
|
|
|
140
|
+
// Hold the first write of a file until asynchronously fetched metadata
|
|
141
|
+
// (cloud) is ready, so the file's metadata line is complete. The wait
|
|
142
|
+
// is bounded twice over: the fetcher's own timeout flips the readiness
|
|
143
|
+
// flag, and the hard deadline covers a callback that never fires.
|
|
144
|
+
if (
|
|
145
|
+
!(opts && opts.force) &&
|
|
146
|
+
this._metadataReady &&
|
|
147
|
+
!this._wroteMetadata &&
|
|
148
|
+
!this._metadataReady() &&
|
|
149
|
+
this._clock().getTime() < this._metadataWaitDeadline
|
|
150
|
+
) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
try {
|
|
136
155
|
this._checkTimeRotation();
|
|
137
156
|
|
|
@@ -181,7 +200,9 @@ class ChannelWriter {
|
|
|
181
200
|
destroy() {
|
|
182
201
|
if (this._destroyed) return;
|
|
183
202
|
this._destroyed = true;
|
|
184
|
-
|
|
203
|
+
// Force: never hold buffered records back at shutdown, even if
|
|
204
|
+
// asynchronously fetched metadata never became ready.
|
|
205
|
+
this.flush({ force: true });
|
|
185
206
|
this.uploadCurrent();
|
|
186
207
|
}
|
|
187
208
|
|
|
@@ -14,6 +14,7 @@ const os = require('os');
|
|
|
14
14
|
const Filters = require('object-filter-sequence');
|
|
15
15
|
|
|
16
16
|
const { ChannelWriter } = require('./channel-writer');
|
|
17
|
+
const { normalizeHost } = require('./s3-uploader');
|
|
17
18
|
|
|
18
19
|
const DEFAULT_FLUSH_INTERVAL_MS = 1000;
|
|
19
20
|
const DEFAULT_ROTATION_SCHEDULE = 'daily';
|
|
@@ -61,6 +62,9 @@ class JsonlFileClient extends EventEmitter {
|
|
|
61
62
|
name: opts.serviceName || 'unknown',
|
|
62
63
|
version: opts.serviceVersion || undefined,
|
|
63
64
|
environment: opts.environment || undefined,
|
|
65
|
+
...(opts.serviceNodeName && {
|
|
66
|
+
node: { configured_name: opts.serviceNodeName },
|
|
67
|
+
}),
|
|
64
68
|
agent: { name: 'tracelog', version: require('../../package').version },
|
|
65
69
|
},
|
|
66
70
|
process: {
|
|
@@ -69,13 +73,18 @@ class JsonlFileClient extends EventEmitter {
|
|
|
69
73
|
argv: process.argv,
|
|
70
74
|
},
|
|
71
75
|
system: {
|
|
72
|
-
|
|
76
|
+
// Normalized the same way as the host in S3 keys, so a viewer
|
|
77
|
+
// can correlate metadata with key-derived hosts using one rule.
|
|
78
|
+
hostname: normalizeHost(os.hostname()),
|
|
73
79
|
architecture: os.arch(),
|
|
74
80
|
platform: os.platform(),
|
|
75
81
|
},
|
|
76
82
|
...(opts.globalLabels && { labels: opts.globalLabels }),
|
|
77
83
|
},
|
|
78
84
|
s3Uploader: opts.s3Uploader || null,
|
|
85
|
+
// Lets writers hold their first write (bounded) until the async
|
|
86
|
+
// cloud-metadata fetch resolves, so metadata lines include `cloud`.
|
|
87
|
+
metadataReady: () => this._cloudMetadataReady,
|
|
79
88
|
maxFileSize: opts.maxFileSize,
|
|
80
89
|
maxBufferSize: opts.maxBufferSize || DEFAULT_MAX_BUFFER_SIZE,
|
|
81
90
|
rotationSchedule: opts.rotationSchedule || DEFAULT_ROTATION_SCHEDULE,
|
|
@@ -211,6 +220,9 @@ class JsonlFileClient extends EventEmitter {
|
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
for (const writer of this._writers.values()) {
|
|
223
|
+
// Not forced: while the (bounded) wait for async cloud metadata is
|
|
224
|
+
// pending, records stay buffered. destroy() is the path that must
|
|
225
|
+
// never hold records back.
|
|
214
226
|
writer.flush();
|
|
215
227
|
}
|
|
216
228
|
|
package/lib/errors.js
CHANGED
|
@@ -288,18 +288,31 @@ Transaction.prototype.toJSON = function () {
|
|
|
288
288
|
};
|
|
289
289
|
|
|
290
290
|
if (this.sampled) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
),
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
291
|
+
// Only include context members that have content — empty placeholder
|
|
292
|
+
// objects are omitted entirely rather than serialized as `{}`.
|
|
293
|
+
const context = {};
|
|
294
|
+
const user = Object.assign(
|
|
295
|
+
{},
|
|
296
|
+
this.req && parsers.getUserContextFromRequest(this.req),
|
|
297
|
+
this._user,
|
|
298
|
+
);
|
|
299
|
+
if (Object.keys(user).length > 0) context.user = user;
|
|
300
|
+
if (this._labels && Object.keys(this._labels).length > 0) {
|
|
301
|
+
context.tags = this._labels;
|
|
302
|
+
}
|
|
303
|
+
if (this._custom && Object.keys(this._custom).length > 0) {
|
|
304
|
+
context.custom = this._custom;
|
|
305
|
+
}
|
|
306
|
+
if (this._service && Object.keys(this._service).length > 0) {
|
|
307
|
+
context.service = this._service;
|
|
308
|
+
}
|
|
309
|
+
if (this._cloud && Object.keys(this._cloud).length > 0) {
|
|
310
|
+
context.cloud = this._cloud;
|
|
311
|
+
}
|
|
312
|
+
if (this._message && Object.keys(this._message).length > 0) {
|
|
313
|
+
context.message = this._message;
|
|
314
|
+
}
|
|
315
|
+
|
|
303
316
|
// Only include dropped count when spans have been dropped.
|
|
304
317
|
if (this._droppedSpans > 0) {
|
|
305
318
|
payload.span_count.dropped = this._droppedSpans;
|
|
@@ -307,14 +320,17 @@ Transaction.prototype.toJSON = function () {
|
|
|
307
320
|
|
|
308
321
|
var conf = this._agent._conf;
|
|
309
322
|
if (this.req) {
|
|
310
|
-
|
|
323
|
+
context.request = parsers.getContextFromRequest(
|
|
311
324
|
this.req,
|
|
312
325
|
conf,
|
|
313
326
|
'transactions',
|
|
314
327
|
);
|
|
315
328
|
}
|
|
316
329
|
if (this.res) {
|
|
317
|
-
|
|
330
|
+
context.response = parsers.getContextFromResponse(this.res, conf);
|
|
331
|
+
}
|
|
332
|
+
if (Object.keys(context).length > 0) {
|
|
333
|
+
payload.context = context;
|
|
318
334
|
}
|
|
319
335
|
}
|
|
320
336
|
|
|
@@ -410,6 +426,19 @@ Transaction.prototype.end = function (result, endTime) {
|
|
|
410
426
|
|
|
411
427
|
if (result !== undefined && result !== null) {
|
|
412
428
|
this.result = result;
|
|
429
|
+
|
|
430
|
+
// Manually ended transactions never go through
|
|
431
|
+
// _setOutcomeFromHttpStatusCode, so derive the outcome from an
|
|
432
|
+
// explicitly passed string result rather than serializing 'unknown'
|
|
433
|
+
// alongside result 'success'. (The result *default* of 'success' does
|
|
434
|
+
// not derive: ending without a result means the outcome is unknown.)
|
|
435
|
+
if (this.outcome === constants.OUTCOME_UNKNOWN && !this._isOutcomeFrozen) {
|
|
436
|
+
if (result === 'success') {
|
|
437
|
+
this.outcome = constants.OUTCOME_SUCCESS;
|
|
438
|
+
} else if (result === 'failure' || result === 'error') {
|
|
439
|
+
this.outcome = constants.OUTCOME_FAILURE;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
413
442
|
}
|
|
414
443
|
|
|
415
444
|
if (!this._defaultName && this.req) this.setDefaultNameFromRequest();
|
package/lib/metrics/registry.js
CHANGED
|
@@ -12,6 +12,7 @@ const { SelfReportingMetricsRegistry } = require('measured-reporting');
|
|
|
12
12
|
const DimensionAwareMetricsRegistry = require('measured-reporting/lib/registries/DimensionAwareMetricsRegistry');
|
|
13
13
|
|
|
14
14
|
const MetricsReporter = require('./reporter');
|
|
15
|
+
const { normalizeHost } = require('../apm-client/s3-uploader');
|
|
15
16
|
const createRuntimeMetrics = require('./runtime');
|
|
16
17
|
const createSystemMetrics =
|
|
17
18
|
process.platform === 'linux'
|
|
@@ -22,7 +23,8 @@ class MetricsRegistry extends SelfReportingMetricsRegistry {
|
|
|
22
23
|
constructor(agent, { reporterOptions, registryOptions = {} } = {}) {
|
|
23
24
|
const defaultReporterOptions = {
|
|
24
25
|
defaultDimensions: {
|
|
25
|
-
|
|
26
|
+
// Normalized the same way as the host in S3 keys (see s3-uploader).
|
|
27
|
+
hostname: normalizeHost(agent._conf.hostname || os.hostname()),
|
|
26
28
|
env: agent._conf.environment || '',
|
|
27
29
|
},
|
|
28
30
|
};
|