@redthreadlabs/tracelog 1.4.0 → 1.5.1

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/index.d.ts CHANGED
@@ -293,6 +293,15 @@ declare namespace apm {
293
293
  spanStackTraceMinDuration?: string;
294
294
  stackTraceLimit?: number;
295
295
  traceContinuationStrategy?: TraceContinuationStrategy;
296
+ /**
297
+ * Route auto-instrumented transactions (and their spans and breakdown
298
+ * metricsets) to a named channel when the transaction name matches a
299
+ * wildcard pattern. First matching rule wins; unmatched records go to
300
+ * the default channel.
301
+ *
302
+ * Example: `[{ pattern: '* unknown route*', channel: 'unknown-route' }]`
303
+ */
304
+ transactionChannels?: Array<TransactionChannelRule>;
296
305
  transactionIgnoreUrls?: Array<string>;
297
306
  transactionMaxSpans?: number;
298
307
  transactionSampleRate?: number;
@@ -329,6 +338,13 @@ declare namespace apm {
329
338
  writeSpan (span: object): void;
330
339
  }
331
340
 
341
+ interface TransactionChannelRule {
342
+ /** Wildcard pattern matched against the transaction name. */
343
+ pattern: string;
344
+ /** Channel to route matching transactions, spans, and breakdown metricsets to. */
345
+ channel: string;
346
+ }
347
+
332
348
  interface CaptureEventOptions {
333
349
  message?: string;
334
350
  level?: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
package/lib/agent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright Shaxpir Inc. and Elasticsearch B.V. and other contributors where applicable.
2
+ * Copyright Red Thread Labs LLC. and Elasticsearch B.V. and other contributors where applicable.
3
3
  * Licensed under the BSD 2-Clause License; you may not use this file except in
4
4
  * compliance with the BSD 2-Clause License.
5
5
  */
@@ -831,6 +831,26 @@ Agent.prototype.writeEvents = function (events, cb) {
831
831
  // the default channel ('server'). Named channels are created lazily via
832
832
  // getChannel(name) and write to separate files (e.g. tracelog-client-*.jsonl).
833
833
 
834
+ /**
835
+ * Resolve the channel that records for the named transaction should be
836
+ * routed to, per the `transactionChannels` config rules (first matching
837
+ * rule wins). Returns null when no rule matches, i.e. the record belongs
838
+ * on the default channel. Used for auto-instrumented transactions, their
839
+ * spans, and their breakdown metricsets.
840
+ */
841
+ Agent.prototype._channelForTransactionName = function (name) {
842
+ const rules = this._conf && this._conf.transactionChannelRules;
843
+ if (!rules || rules.length === 0 || typeof name !== 'string') {
844
+ return null;
845
+ }
846
+ for (let i = 0; i < rules.length; i++) {
847
+ if (rules[i].re.test(name)) {
848
+ return rules[i].channel;
849
+ }
850
+ }
851
+ return null;
852
+ };
853
+
834
854
  /**
835
855
  * Get a named channel. Returns an object with writeEvent, writeEvents,
836
856
  * writeError, writeTransaction, and writeSpan methods that route to the
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Copyright Elasticsearch B.V. and other contributors where applicable.
3
- * Copyright Shaxpir Inc. All rights reserved.
3
+ * Copyright Red Thread Labs LLC. All rights reserved.
4
4
  * Licensed under the BSD 2-Clause License; you may not use this file except in
5
5
  * compliance with the BSD 2-Clause License.
6
6
  */
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright Shaxpir Inc. All rights reserved.
2
+ * Copyright Red Thread Labs LLC. All rights reserved.
3
3
  * Licensed under the BSD 2-Clause License; you may not use this file except in
4
4
  * compliance with the BSD 2-Clause License.
5
5
  */
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Copyright Elasticsearch B.V. and other contributors where applicable.
3
- * Copyright Shaxpir Inc. All rights reserved.
3
+ * Copyright Red Thread Labs LLC. All rights reserved.
4
4
  * Licensed under the BSD 2-Clause License; you may not use this file except in
5
5
  * compliance with the BSD 2-Clause License.
6
6
  */
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright Shaxpir Inc. All rights reserved.
2
+ * Copyright Red Thread Labs LLC. All rights reserved.
3
3
  * Licensed under the BSD 2-Clause License; you may not use this file except in
4
4
  * compliance with the BSD 2-Clause License.
5
5
  */
@@ -48,6 +48,7 @@ const {
48
48
  normalizeSanitizeFieldNames,
49
49
  normalizeCloudProvider,
50
50
  normalizeCustomMetricsHistogramBoundaries,
51
+ normalizeTransactionChannels,
51
52
  normalizeTransactionSampleRate,
52
53
  normalizeTraceContinuationStrategy,
53
54
  normalizeContextManager,
@@ -396,6 +397,7 @@ function normalize(opts, logger) {
396
397
  normalizeDurationOptions(opts, DURATION_OPTS, defaults, logger);
397
398
  normalizeBools(opts, BOOL_OPTS, defaults, logger);
398
399
  normalizeIgnoreOptions(opts);
400
+ normalizeTransactionChannels(opts, [], defaults, logger);
399
401
  normalizeElasticsearchCaptureBodyUrls(opts);
400
402
  normalizeDisableMetrics(opts);
401
403
  normalizeSanitizeFieldNames(opts);
@@ -427,6 +427,46 @@ function normalizeIgnoreOptions(opts, fields, defaults, logger) {
427
427
  }
428
428
  }
429
429
 
430
+ /**
431
+ * Normalizes transactionChannels rules into compiled wildcard matchers.
432
+ *
433
+ * Input is an array of { pattern, channel } objects, where `pattern` is a
434
+ * wildcard expression matched against the transaction name (e.g.
435
+ * '* unknown route*') and `channel` is the name of the channel to route
436
+ * matching transactions (and their spans and breakdown metricsets) to.
437
+ * Compiles to opts.transactionChannelRules: [{ re, channel }]. Invalid
438
+ * rules are dropped with a warning.
439
+ *
440
+ * @param {Record<String, unknown>} opts the configuration options to normalize
441
+ * @param {String[]} fields the list of fields to normalize (unused)
442
+ * @param {Record<String, unknown>} defaults the configuration defaults (unused)
443
+ * @param {import('../logging.js').Logger} logger
444
+ */
445
+ function normalizeTransactionChannels(opts, fields, defaults, logger) {
446
+ if (opts.transactionChannels) {
447
+ opts.transactionChannelRules = [];
448
+ const wildcard = new WildcardMatcher();
449
+ for (const rule of opts.transactionChannels) {
450
+ if (
451
+ !rule ||
452
+ typeof rule.pattern !== 'string' ||
453
+ typeof rule.channel !== 'string' ||
454
+ !rule.channel
455
+ ) {
456
+ logger.warn(
457
+ 'invalid transactionChannels rule (expected { pattern, channel } strings): %j',
458
+ rule,
459
+ );
460
+ continue;
461
+ }
462
+ opts.transactionChannelRules.push({
463
+ re: wildcard.compile(rule.pattern),
464
+ channel: rule.channel,
465
+ });
466
+ }
467
+ }
468
+ }
469
+
430
470
  /**
431
471
  * Normalizes the wildcard matchers of sanitizeFieldNames and thansforms the into RegExps
432
472
  *
@@ -640,6 +680,7 @@ module.exports = {
640
680
  normalizeKeyValuePairs,
641
681
  normalizeNumbers,
642
682
  normalizeSanitizeFieldNames,
683
+ normalizeTransactionChannels,
643
684
  normalizeTransactionSampleRate,
644
685
  secondsFromDuration,
645
686
  normalizeTraceContinuationStrategy,
@@ -361,6 +361,17 @@ const CONFIG_SCHEMA = [
361
361
  centralConfigName: 'trace_continuation_strategy',
362
362
  crossAgentName: 'trace_continuation_strategy',
363
363
  },
364
+ {
365
+ name: 'transactionChannels',
366
+ configType: 'transactionChannelArray',
367
+ defaultValue: [],
368
+ },
369
+ {
370
+ name: 'transactionChannelRules',
371
+ configType: 'wildcardChannelRuleArray',
372
+ defaultValue: [],
373
+ deps: ['transactionChannels'],
374
+ },
364
375
  {
365
376
  name: 'transactionIgnoreUrls',
366
377
  configType: 'stringArray',
@@ -710,6 +710,14 @@ Instrumentation.prototype.addEndedTransaction = function (transaction) {
710
710
  trans: transaction.id,
711
711
  trace: transaction.traceId,
712
712
  });
713
+ // The transaction's name is final now, so held spans can be routed
714
+ // even though the transaction record itself was dropped.
715
+ this._drainPendingChannelSpans(
716
+ transaction,
717
+ agent._channelForTransactionName(
718
+ transaction._customName || transaction._defaultName,
719
+ ),
720
+ );
713
721
  return;
714
722
  }
715
723
 
@@ -717,7 +725,40 @@ Instrumentation.prototype.addEndedTransaction = function (transaction) {
717
725
  trans: transaction.id,
718
726
  trace: transaction.traceId,
719
727
  });
720
- agent._apmClient.sendTransaction(payload);
728
+ const channel = agent._channelForTransactionName(payload.name);
729
+ // Spans held while this transaction's name was unresolved follow the
730
+ // same routing decision as the transaction itself.
731
+ this._drainPendingChannelSpans(transaction, channel);
732
+ if (channel && typeof agent._apmClient.sendToChannel === 'function') {
733
+ agent._apmClient.sendToChannel(channel, 'transaction', payload);
734
+ } else {
735
+ agent._apmClient.sendTransaction(payload);
736
+ }
737
+ };
738
+
739
+ // Send any span payloads that were held on the transaction because the
740
+ // transaction's name (and so its channel routing) was not yet resolved
741
+ // when the span ended. See `_encodeAndSendSpan`.
742
+ Instrumentation.prototype._drainPendingChannelSpans = function (
743
+ transaction,
744
+ channel,
745
+ ) {
746
+ const agent = this._agent;
747
+ const pending = transaction._pendingChannelSpans;
748
+ if (!pending) {
749
+ return;
750
+ }
751
+ transaction._pendingChannelSpans = null;
752
+ if (!agent._apmClient) {
753
+ return;
754
+ }
755
+ for (const payload of pending) {
756
+ if (channel && typeof agent._apmClient.sendToChannel === 'function') {
757
+ agent._apmClient.sendToChannel(channel, 'span', payload);
758
+ } else {
759
+ agent._apmClient.sendSpan(payload);
760
+ }
761
+ }
721
762
  };
722
763
 
723
764
  Instrumentation.prototype.addEndedSpan = function (span) {
@@ -841,7 +882,40 @@ Instrumentation.prototype._encodeAndSendSpan = function (span) {
841
882
  type: span.type,
842
883
  });
843
884
  if (agent._apmClient) {
844
- agent._apmClient.sendSpan(payload);
885
+ // Spans follow their transaction's channel. The transaction name
886
+ // is only meaningful for routing once *resolved* (custom or
887
+ // framework-set); the `name` getter must not be used here, since
888
+ // until the transaction ends it falls back to
889
+ // '<METHOD> unknown route (unnamed)', which would spuriously
890
+ // match rules aimed at unmatched-route traffic. When routing is
891
+ // configured and the name is not yet resolved (e.g. Express only
892
+ // names transactions when the request finishes), hold the span on
893
+ // its transaction; addEndedTransaction() drains held spans with
894
+ // the transaction's final routing decision.
895
+ const trans = span.transaction;
896
+ const routingEnabled =
897
+ agent._conf.transactionChannelRules &&
898
+ agent._conf.transactionChannelRules.length > 0 &&
899
+ typeof agent._apmClient.sendToChannel === 'function';
900
+ if (!routingEnabled) {
901
+ agent._apmClient.sendSpan(payload);
902
+ } else {
903
+ const resolvedName =
904
+ trans && (trans._customName || trans._defaultName);
905
+ if (!resolvedName && trans && !trans.ended) {
906
+ if (!trans._pendingChannelSpans) {
907
+ trans._pendingChannelSpans = [];
908
+ }
909
+ trans._pendingChannelSpans.push(payload);
910
+ } else {
911
+ const channel = agent._channelForTransactionName(resolvedName);
912
+ if (channel) {
913
+ agent._apmClient.sendToChannel(channel, 'span', payload);
914
+ } else {
915
+ agent._apmClient.sendSpan(payload);
916
+ }
917
+ }
918
+ }
845
919
  }
846
920
  }
847
921
  }
@@ -67,7 +67,19 @@ class MetricsReporter extends Reporter {
67
67
 
68
68
  if (this._agent._apmClient) {
69
69
  for (const metric of seen.values()) {
70
- this._agent._apmClient.sendMetricSet(metric);
70
+ // Breakdown metricsets carry the name of the transaction they
71
+ // were aggregated for; route them to that transaction's channel.
72
+ const channel = this._agent._channelForTransactionName(
73
+ metric.transaction && metric.transaction.name,
74
+ );
75
+ if (
76
+ channel &&
77
+ typeof this._agent._apmClient.sendToChannel === 'function'
78
+ ) {
79
+ this._agent._apmClient.sendToChannel(channel, 'metricset', metric);
80
+ } else {
81
+ this._agent._apmClient.sendMetricSet(metric);
82
+ }
71
83
  }
72
84
  }
73
85
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redthreadlabs/tracelog",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Node.js APM instrumentation that writes traces to JSONL files",
5
5
  "publishConfig": {
6
6
  "access": "public"