/
shim.js
2107 lines (1939 loc) · 65.9 KB
/
shim.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const arity = require('../util/arity')
const hasOwnProperty = require('../util/properties').hasOwn
const logger = require('../logger').child({ component: 'Shim' })
const path = require('path')
const specs = require('./specs')
const util = require('util')
const symbols = require('../symbols')
const { addCLMAttributes: maybeAddCLMAttributes } = require('../util/code-level-metrics')
const { makeId } = require('../util/hashes')
const { isBuiltin } = require('module')
// Some modules do terrible things, like change the prototype of functions. To
// avoid crashing things we'll use a cached copy of apply everywhere.
const fnApply = Function.prototype.apply
/**
* Constructs a shim associated with the given agent instance.
*
* @class
* @classdesc A helper class for wrapping modules with segments.
* @param {Agent} agent - The agent this shim will use.
* @param {string} moduleName - The name of the module being instrumented.
* @param {string} resolvedName - The full path to the loaded module.
* @param {string} shimName - Used to persist shim ids across different instances. This is
* @param {string} pkgVersion - version of package getting instrumented
* applicable to instrument that compliments each other across libraries(i.e - koa + koa-route/koa-router)
*/
function Shim(agent, moduleName, resolvedName, shimName, pkgVersion) {
if (!agent || !moduleName) {
throw new Error('Shim must be initialized with an agent and module name.')
}
this._logger = logger.child({ module: moduleName })
this._agent = agent
this._contextManager = agent._contextManager
this._toExport = null
this._debug = false
this.defineProperty(this, 'moduleName', moduleName)
this.assignId(shimName)
this.pkgVersion = pkgVersion
// Used in `shim.require`
// If this is a built-in the root is set as `.`
this._moduleRoot = isBuiltin(resolvedName || moduleName) ? '.' : resolvedName
}
module.exports = Shim
Shim.defineProperty = defineProperty
Shim.defineProperties = defineProperties
// Copy the argument index enumeration onto the shim.
Shim.prototype.ARG_INDEXES = specs.ARG_INDEXES
defineProperties(Shim.prototype, specs.ARG_INDEXES)
// Define other miscellaneous properties of the shim.
defineProperties(Shim.prototype, {
/**
* The agent associated with this shim.
*
* @readonly
* @member {Agent} Shim.prototype.agent
* @returns {Agent} The instance of the agent.
*/
agent: function getAgent() {
return this._agent
},
/**
* The transaction tracer in use by the agent for the shim.
*
* @readonly
* @member {Tracer} Shim.prototype.tracer
* @returns {Tracer} The agent's instance of the tracer
*/
tracer: function getTracer() {
return this._agent.tracer
},
/**
* The logger for this shim.
*
* @readonly
* @member {Logger} Shim.prototype.logger
* @returns {Logger} The logger.
*/
logger: function getLogger() {
return this._logger
}
})
Shim.prototype.wrap = wrap
Shim.prototype.bindSegment = bindSegment
Shim.prototype.bindPromise = bindPromise
Shim.prototype.execute = execute
Shim.prototype.wrapReturn = wrapReturn
Shim.prototype.wrapClass = wrapClass
Shim.prototype.wrapExport = wrapExport
Shim.prototype.record = record
Shim.prototype.isWrapped = isWrapped
Shim.prototype.unwrap = unwrap
Shim.prototype.unwrapOnce = unwrap
Shim.prototype.getOriginal = getOriginal
Shim.prototype.getOriginalOnce = getOriginalOnce
Shim.prototype.assignOriginal = assignOriginal
Shim.prototype.getSegment = getSegment
Shim.prototype.getActiveSegment = getActiveSegment
Shim.prototype.setActiveSegment = setActiveSegment
Shim.prototype.storeSegment = storeSegment
Shim.prototype.bindCallbackSegment = bindCallbackSegment
Shim.prototype.applySegment = applySegment
Shim.prototype.createSegment = createSegment
Shim.prototype.getName = getName
Shim.prototype.isObject = isObject
Shim.prototype.isFunction = isFunction
Shim.prototype.isPromise = isPromise
Shim.prototype.isAsyncFunction = isAsyncFunction
Shim.prototype.isString = isString
Shim.prototype.isNumber = isNumber
Shim.prototype.isBoolean = isBoolean
Shim.prototype.isArray = isArray
Shim.prototype.isNull = isNull
Shim.prototype.toArray = toArray
Shim.prototype.argsToArray = argsToArray
Shim.prototype.normalizeIndex = normalizeIndex
Shim.prototype.once = once
Shim.prototype.defineProperty = defineProperty
Shim.prototype.defineProperties = defineProperties
Shim.prototype.setDefaults = setDefaults
Shim.prototype.proxy = proxy
Shim.prototype.require = shimRequire
Shim.prototype.copySegmentParameters = copySegmentParameters
Shim.prototype.prefixRouteParameters = prefixRouteParameters
Shim.prototype.interceptPromise = interceptPromise
Shim.prototype.fixArity = arity.fixArity
Shim.prototype.assignId = assignId
Shim.prototype.specs = specs
// Internal methods.
Shim.prototype.getExport = getExport
Shim.prototype.enableDebug = enableDebug
Shim.prototype[symbols.unwrap] = unwrapAll
// -------------------------------------------------------------------------- //
/**
* @callback WrapFunction
* @summary
* A function which performs the actual wrapping logic.
* @description
* If the return value of this function is not `original` then the return value
* will be marked as a wrapper.
* @param {Shim} shim
* The shim this function was passed to.
* @param {object|Function} original
* The item which needs wrapping. Most of the time this will be a function.
* @param {string} name
* The name of `original` if it can be determined, otherwise `'<anonymous>'`.
* @returns {*} The wrapper for the original, or the original value itself.
*/
/**
* @private
* @callback ArrayWrapFunction
* @description
* A wrap function used on elements of an array. In addition to the parameters
* of `WrapFunction`, these also receive an `index` and `total` as described
* below.
* @see WrapFunction
* @param {number} index - The index of the current element in the array.
* @param {number} total - The total number of items in the array.
*/
/**
* @private
* @callback ArgumentsFunction
* @param {Shim} shim
* The shim this function was passed to.
* @param {Function} func
* The function these arguments were passed to.
* @param {*} context
* The context the function is executing under (i.e. `this`).
* @param {Array.<*>} args
* The arguments being passed into the function.
*/
/**
* @callback SegmentFunction
* @summary
* A function which is called to compose a segment.
* @param {Shim} shim
* The shim this function was passed to.
* @param {Function} func
* The function the segment is created for.
* @param {string} name
* The name of the function.
* @param {Array.<*>} args
* The arguments being passed into the function.
* @returns {string|SegmentSpec} The desired properties for the new segment.
*/
/**
* @callback RecorderFunction
* @summary
* A function which is called to compose a segment for recording.
* @param {Shim} shim
* The shim this function was passed to.
* @param {Function} func
* The function being recorded.
* @param {string} name
* The name of the function.
* @param {Array.<*>} args
* The arguments being passed into the function.
* @returns {string|RecorderSpec} The desired properties for the new segment.
*/
/**
* @callback CallbackBindFunction
* @summary
* Performs segment binding on a callback function. Useful when identifying a
* callback is more complex than a simple argument offset.
* @param {Shim} shim
* The shim this function was passed to.
* @param {Function} func
* The function being recorded.
* @param {string} name
* The name of the function.
* @param {TraceSegment} segment
* The segment that the callback should be bound to.
* @param {Array.<*>} args
* The arguments being passed into the function.
*/
/**
* @private
* @callback MetricFunction
* @summary
* Measures all the necessary metrics for the given segment. This functionality
* is meant to be used by Shim subclasses, instrumentations should never create
* their own recorders.
* @param {TraceSegment} segment - The segment to record.
* @param {string} [scope] - The scope of the recording.
*/
// -------------------------------------------------------------------------- //
/**
* Entry point for executing a spec.
*
* @param {object|Function} nodule Class or module containing the function to wrap.
* @param {Spec} spec {@link Spec}
* @memberof Shim.prototype
*/
function execute(nodule, spec) {
if (this.isFunction(spec)) {
spec(this, nodule)
} else {
_specToFunction(spec)
}
}
/**
* Executes the provided spec on one or more objects.
*
* - `wrap(nodule, properties, spec [, args])`
* - `wrap(func, spec [, args])`
*
* When called with a `nodule` and one or more properties, the spec will be
* executed on each property listed and the return value put back on the
* `nodule`.
*
* When called with just a function, the spec will be executed on the function
* and the return value of the spec simply passed back.
*
* The wrapped version will have the same prototype as the original
* method.
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source for the properties to wrap, or a single function to wrap.
* @param {string|Array.<string>} [properties]
* One or more properties to wrap. If omitted, the `nodule` parameter is
* assumed to be the function to wrap.
* @param {Spec|WrapFunction} spec
* The spec for wrapping these items.
* @param {Array.<*>} [args]
* Optional extra arguments to be sent to the spec when executing it.
* @returns {object | Function} The first parameter to this function, after
* wrapping it or its properties.
* @see WrapFunction
*/
function wrap(nodule, properties, spec, args) {
if (!nodule) {
this.logger.debug('Not wrapping non-existent nodule.')
return nodule
}
// Sort out the parameters.
if (this.isObject(properties) && !this.isArray(properties)) {
// wrap(nodule, spec [, args])
args = spec
spec = properties
properties = null
}
if (this.isFunction(spec)) {
// wrap(nodule [, properties], wrapper [, args])
spec = new specs.WrapSpec({
wrapper: spec
})
}
// If we're just wrapping one thing, just wrap it and return.
if (properties == null) {
const name = this.getName(nodule)
this.logger.trace('Wrapping nodule itself (%s).', name)
return _wrap(this, nodule, name, spec, args)
}
// Coerce properties into an array.
if (!this.isArray(properties)) {
properties = [properties]
}
// Wrap each property and return the nodule.
this.logger.trace('Wrapping %d properties on nodule.', properties.length)
properties.forEach(function wrapEachProperty(prop) {
// Skip nonexistent properties.
const original = nodule[prop]
if (!original) {
this.logger.debug('Not wrapping missing property "%s"', prop)
return
}
// Wrap up the property and add a special unwrapper.
const wrapped = _wrap(this, original, prop, spec, args)
if (wrapped && wrapped !== original) {
this.logger.trace('Replacing "%s" with wrapped version', prop)
nodule[prop] = wrapped
wrapped[symbols.unwrap] = function unwrapWrap() {
nodule[prop] = original
return original
}
}
}, this)
return nodule
}
/**
* Executes the provided spec with the return value of the given properties.
*
* - `wrapReturn(nodule, properties, spec [, args])`
* - `wrapReturn(func, spec [, args])`
*
* If the wrapper is executed with `new` then the wrapped function will also be
* called with `new`. This feature should only be used with factory methods
* disguised as classes. Normally {@link Shim#wrapClass} should be used to wrap
* constructors instead.
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source for the properties to wrap, or a single function to wrap.
* @param {string|Array.<string>} [properties]
* One or more properties to wrap. If omitted, the `nodule` parameter is
* assumed to be the function to wrap.
* @param {Spec|Function} spec
* The spec for wrapping the returned value from the properties.
* @param {Array.<*>} [args]
* Optional extra arguments to be sent to the spec when executing it.
* @returns {object | Function} The first parameter to this function, after
* wrapping it or its properties.
* @see Shim#wrap
*/
function wrapReturn(nodule, properties, spec, args) {
// Munge our parameters as needed.
if (this.isObject(properties) && !this.isArray(properties)) {
// wrapReturn(nodule, spec [, args])
args = spec
spec = properties
properties = null
}
if (!this.isFunction(spec)) {
_specToFunction(spec)
}
if (!this.isArray(args)) {
args = []
}
// Perform the wrapping!
return this.wrap(nodule, properties, function returnWrapper(shim, fn, fnName) {
// Only functions can have return values for us to wrap.
if (!shim.isFunction(fn)) {
return fn
}
return wrapInProxy({ fn, fnName, shim, args, spec })
})
}
/**
* Wraps a function in a proxy with various handlers
*
* @private
* @param {object} params to function
* @param {Function} params.fn function to wrap in Proxy(return of function invocation)
* @param {string} params.fnName name of function
* @param {Shim} params.shim instance of shim
* @param {Array} params.args args to original caller function
* @param {Spec} params.spec the spec for wrapping the returned value
* @returns {Proxy} proxied return function
*/
function wrapInProxy({ fn, fnName, shim, args, spec }) {
let unwrapReference = null
const handler = {
get: function getTrap(target, prop) {
// The wrapped symbol only lives on proxy
// not the proxied item.
if (prop === symbols.wrapped) {
return this[prop]
}
// Allow for look up of the target
if (prop === symbols.original) {
return target
}
if (prop === symbols.unwrap) {
return unwrapReference
}
return target[prop]
},
defineProperty: function definePropertyTrap(target, key, descriptor) {
if (key === symbols.unwrap) {
unwrapReference = descriptor.value
} else {
Object.defineProperty(target, key, descriptor)
}
return true
},
set: function setTrap(target, key, val) {
// If we are setting the wrapped symbol on proxy
// we do not actually want to assign to proxied
// item but the proxy itself.
if (key === symbols.wrapped) {
this[key] = val
} else if (key === symbols.unwrap) {
unwrapReference = val
} else {
target[key] = val
}
return true
},
construct: function constructTrap(target, proxyArgs, newTarget) {
// Call the underlying function via Reflect.
let ret = Reflect.construct(target, proxyArgs, newTarget)
// Assemble the arguments to hand to the spec.
const _args = [shim, fn, fnName, ret]
if (args.length > 0) {
_args.push.apply(_args, args)
}
// Call the spec and see if it handed back a different return value.
const newRet = spec.apply(ret, _args)
if (newRet) {
ret = newRet
}
return ret
},
apply: function applyTrap(target, thisArg, proxyArgs) {
// Call the underlying function. If this was called as a constructor, call
// the wrapped function as a constructor too.
let ret = target.apply(thisArg, proxyArgs)
// Assemble the arguments to hand to the spec.
const _args = [shim, fn, fnName, ret]
if (args.length > 0) {
_args.push.apply(_args, args)
}
// Call the spec and see if it handed back a different return value.
const newRet = spec.apply(thisArg, _args)
if (newRet) {
ret = newRet
}
return ret
}
}
return new Proxy(fn, handler)
}
/**
* Wraps a class constructor using a subclass with pre- and post-construction
* hooks.
*
* - `wrapClass(nodule, properties, spec [, args])`
* - `wrapClass(func, spec [, args])`
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source for the properties to wrap, or a single function to wrap.
* @param {string|Array.<string>} [properties]
* One or more properties to wrap. If omitted, the `nodule` parameter is
* assumed to be the constructor to wrap.
* @param {ClassWrapSpec|ConstructorHookFunction} spec
* The spec for wrapping the returned value from the properties or a post hook.
* @param {Array.<*>} [args]
* Optional extra arguments to be sent to the spec when executing it.
* @returns {object | Function} The first parameter to this function, after
* wrapping it or its properties.
* @see Shim#wrap
*/
function wrapClass(nodule, properties, spec, args) {
// Munge our parameters as needed.
if (this.isObject(properties) && !this.isArray(properties)) {
// wrapReturn(nodule, spec [, args])
args = spec
spec = properties
properties = null
}
if (!this.isArray(args)) {
args = []
}
// Perform the wrapping!
return this.wrap(nodule, properties, function classWrapper(shim, Base, fnName) {
// Only functions can have return values for us to wrap.
if (!shim.isFunction(Base) || shim.isWrapped(Base)) {
return Base
}
// When es6 classes are being wrapped, we need to use an es6 class due to
// the fact our es5 wrapper depends on calling the constructor without `new`.
const wrapper = spec.es6 || /^class /.test(Base.toString()) ? _es6WrapClass : _es5WrapClass
return wrapper(shim, Base, fnName, spec, args)
})
}
/**
* Wraps the actual module being instrumented to change what `require` returns.
*
* - `wrapExport(nodule, spec)`
*
* @memberof Shim.prototype
* @param {*} nodule
* The original export to replace with our new one.
* @param {WrapFunction} spec
* A wrapper function. The return value from this spec is what will replace
* the export.
* @returns {*} The return value from `spec`.
*/
function wrapExport(nodule, spec) {
if (nodule[symbols.nrEsmProxy] === true) {
// A CJS module has been imported as ESM through import-in-the-middle. This
// means that `nodule` is set to an instance of our proxy. What we actually
// want is the thing to be instrumented. We assume it is the "default"
// export.
nodule = nodule.default
}
return (this._toExport = this.wrap(nodule, null, spec))
}
/**
* If the export was wrapped, that wrapper is returned, otherwise `defaultExport`.
*
* @private
* @memberof Shim.prototype
* @param {*} defaultExport - The original export in case it was never wrapped.
* @returns {*} The result from calling {@link Shim#wrapExport} or `defaultExport`
* if it was never used.
* @see Shim.wrapExport
*/
function getExport(defaultExport) {
return this._toExport || defaultExport
}
/**
* Determines if the specified function or property exists and is wrapped.
*
* - `isWrapped(nodule, property)`
* - `isWrapped(func)`
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source for the property or a single function to check.
* @param {string} [property]
* The property to check. If omitted, the `nodule` parameter is assumed to be
* the function to check.
* @returns {boolean} True if the item exists and has been wrapped.
* @see Shim#wrap
* @see Shim#bindSegment
*/
function isWrapped(nodule, property) {
if (property) {
return !!(nodule?.[property]?.[symbols.wrapped] === this.id)
}
return !!(nodule?.[symbols.wrapped] === this.id)
}
/**
* Wraps a function with segment creation and binding.
*
* - `record(nodule, properties, recordNamer)`
* - `record(func, recordNamer)`
*
* This is shorthand for calling {@link Shim#wrap} and manually creating a segment.
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source for the properties to record, or a single function to record.
* @param {string|Array.<string>} [properties]
* One or more properties to record. If omitted, the `nodule` parameter is
* assumed to be the function to record.
* @param {RecorderFunction} recordNamer
* A function which returns a record descriptor that gives the name and type of
* record we'll make.
* @returns {object | Function} The first parameter, possibly wrapped.
* @see RecorderFunction
* @see RecorderSpec
* @see Shim#wrap
*/
function record(nodule, properties, recordNamer) {
if (this.isFunction(properties)) {
recordNamer = properties
properties = null
}
return this.wrap(nodule, properties, function makeWrapper(shim, fn, name) {
// Can't record things that aren't functions.
if (!shim.isFunction(fn)) {
shim.logger.debug('Not recording non-function "%s".', name)
return fn
}
shim.logger.trace('Wrapping "%s" with metric recording.', name)
return recordWrapper({ shim, fn, name, recordNamer })
})
}
/**
* Wrapped function for Shim.prototype.record
* This creates a segment for the method being recorded
*
* @private
* @param {object} params to function
* @param {Shim} params.shim instance of shim
* @param {Function} params.fn function being wrapped/recorded
* @param {string} params.name name of function
* @param {RecorderFunction} params.recordNamer
* A function which returns a record descriptor that gives the name and type of
* record we'll make.
* @returns {Function} wrapped function
*/
function recordWrapper({ shim, fn, name, recordNamer }) {
return function wrapper() {
// Create the segment that will be recorded.
const args = argsToArray.apply(shim, arguments)
const segDesc = recordNamer.call(this, shim, fn, name, args)
if (!segDesc) {
shim.logger.trace('No segment descriptor for "%s", not recording.', name)
return fnApply.call(fn, this, args)
}
// See if we're in an active transaction.
let parent
if (segDesc.parent) {
// We only want to continue recording in a transaction if the
// transaction is active.
parent = segDesc.parent.transaction.isActive() ? segDesc.parent : null
} else {
parent = shim.getActiveSegment()
}
if (!parent) {
shim.logger.debug('Not recording function %s, not in a transaction.', name)
return fnApply.call(fn, this, arguments)
}
if (segDesc.callbackRequired && !_hasValidCallbackArg(shim, args, segDesc.callback)) {
return fnApply.call(fn, this, arguments)
}
// Only create a segment if:
// - We are _not_ making an internal segment.
// - OR the parent segment is either not internal or not from this shim.
const shouldCreateSegment = !(
parent.opaque ||
(segDesc.internal && parent.internal && shim === parent.shim)
)
const segment = shouldCreateSegment ? _rawCreateSegment(shim, segDesc) : parent
maybeAddCLMAttributes(fn, segment)
return _doRecord.call(this, { segment, args, segDesc, shouldCreateSegment, shim, fn, name })
}
}
/**
* Check if the argument defined as callback is an actual function
*
* @private
* @param {Shim} shim An instance of the shim class
* @param {Array} args The arguments to the wrapped function
* @param {Function} specCallback Optional callback argument received from the spec
* @returns {boolean} Whether the spec ha a valid callback argument
*/
function _hasValidCallbackArg(shim, args, specCallback) {
if (shim.isNumber(specCallback)) {
const cbIdx = normalizeIndex(args.length, specCallback)
if (cbIdx === null) {
return false
}
const callback = args[cbIdx]
return shim.isFunction(callback)
}
return true
}
/**
* Binds all callbacks, streams and/or returned promises to the active segment of function being wrapped.
*
* @private
* @param {object} params to function
* @param {TraceSegment} params.segment The trace segment to be recorded
* @param {Array} params.args The arguments to the wrapped callback
* @param {Spec} params.segDesc Segment descriptor spec
* @param {boolean} params.shouldCreateSegment Whether the recorder should create a segment
* @param {Shim} params.shim instance of shim
* @param {Function} params.fn function being wrapped
* @param {string} params.name name of function being wrapped
* @returns {shim|promise} Returns a shim or promise with recorder segment and
* bound callbacks, if applicable
*/
function _doRecord({ segment, args, segDesc, shouldCreateSegment, shim, fn, name }) {
// Now bind any callbacks specified in the segment descriptor.
_bindAllCallbacks.call(this, shim, fn, name, args, {
spec: segDesc,
segment: segment,
shouldCreateSegment: shouldCreateSegment
})
// Apply the function, and (if it returned a stream) bind that too.
// The reason there is no check for `segment` is because it should
// be guaranteed by the parent and active transaction check
// at the beginning of this function.
let ret = _applyRecorderSegment({ segment, ctx: this, args, segDesc, shim, fn, name })
if (ret) {
if (segDesc.stream) {
shim.logger.trace('Binding return value as stream.')
_bindStream(shim, ret, segment, {
event: shim.isString(segDesc.stream) ? segDesc.stream : null,
shouldCreateSegment: shouldCreateSegment
})
} else if (segDesc.promise && shim.isPromise(ret)) {
shim.logger.trace('Binding return value as Promise.')
ret = shim.bindPromise(ret, segment)
}
}
return ret
}
/**
* Binds active segment to wrapped function. Calls the after hook if it exists on spec
*
* @private
* @param {object} params to function
* @param {TraceSegment} params.segment The trace segment being applied to the wrapped function
* @param {context} params.ctx Context supplied to
* @param {Array} params.args The arguments to the wrapped callback
* @param {Spec} params.segDesc Segment descriptor spec
* @param {Shim} params.shim instance of shim
* @param {Function} params.fn function being wrapped
* @param {string} params.name name of function being wrapped
* @returns {*} return value of wrapped function
*/
function _applyRecorderSegment({ segment, ctx, args, segDesc, shim, fn, name }) {
let error = null
let promised = false
let ret
try {
ret = shim.applySegment(fn, segment, true, ctx, args, segDesc.inContext)
if (segDesc.after && segDesc.promise && shim.isPromise(ret)) {
promised = true
return ret.then(
function onThen(val) {
segment.touch()
// passing in error as some instrumentation checks if it's not equal to `null`
segDesc.after({ shim, fn, name, error, result: val, segment })
return val
},
function onCatch(err) {
segment.touch()
segDesc.after({ shim, fn, name, error: err, segment })
throw err // NOTE: This is not an error from our instrumentation.
}
)
}
return ret
} catch (err) {
error = err
throw err // Just rethrowing this error, not our error!
} finally {
if (segDesc.after && (error || !promised)) {
segDesc.after({ shim, fn, name, error, result: ret, segment })
}
}
}
/**
* Unwraps one item, revealing the underlying value. If item is wrapped multiple times,
* the unwrap will not occur as we cannot safely unwrap.
*
* - `unwrap(nodule, property)`
* - `unwrap(func)`
*
* If called with a `nodule` and properties, the unwrapped value will be put
* back on the nodule. Otherwise, the unwrapped function is just returned.
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source for the properties to unwrap, or a single function to unwrap.
* @param {string|Array.<string>} [properties]
* One or more properties to unwrap. If omitted, the `nodule` parameter is
* assumed to be the function to unwrap.
* @returns {object | Function} The first parameter after unwrapping.
*/
function unwrap(nodule, properties) {
// Don't try to unwrap potentially `null` or `undefined` things.
if (!nodule) {
return nodule
}
// If we're unwrapping multiple things
if (this.isArray(properties)) {
properties.forEach(unwrap.bind(this, nodule))
return nodule
}
const unwrapObj = properties || '<nodule>'
this.logger.trace('Unwrapping %s', unwrapObj)
const original = properties ? nodule[properties] : nodule
if (!original || (original && !original[symbols.original])) {
return original
} else if (original?.[symbols.original]?.[symbols.original]) {
this.logger.warn(
'Attempting to unwrap %s, which its unwrapped version is also wrapped. This is unsupported, unwrap will not occur.',
unwrapObj
)
return original
}
return this.isFunction(original[symbols.unwrap])
? original[symbols.unwrap]()
: original[symbols.original]
}
/**
* Retrieves the original method for a wrapped function.
*
* - `getOriginal(nodule, property)`
* - `getOriginal(func)`
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source of the property to get the original of, or a function to unwrap.
* @param {string} [property]
* A property on `nodule` to get the original value of.
* @returns {object | Function} The original value for the given item.
*/
function getOriginal(nodule, property) {
if (!nodule) {
return nodule
}
let original = property ? nodule[property] : nodule
while (original && original[symbols.original]) {
original = original[symbols.original]
}
return original
}
/**
* Retrieves the value of symbols.original on the wrapped function.
* Unlike `getOriginal` this just looks in the direct wrapped function
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source of the property to get the original of, or a function to unwrap.
* @param {string} [property]
* A property on `nodule` to get the original value of.
* @returns {object | Function} The original value for the given item.
*/
function getOriginalOnce(nodule, property) {
if (!nodule) {
return nodule
}
const original = property ? nodule[property] : nodule
return original[symbols.original]
}
/**
* Binds the execution of a function to a single segment.
*
* - `bindSegment(nodule , property [, segment [, full]])`
* - `bindSegment(func [, segment [, full]])`
*
* If called with a `nodule` and a property, the wrapped property will be put
* back on the nodule. Otherwise, the wrapped function is just returned.
*
* @memberof Shim.prototype
* @param {object | Function} nodule
* The source for the property or a single function to bind to a segment.
* @param {string} [property]
* The property to bind. If omitted, the `nodule` parameter is assumed
* to be the function to bind the segment to.
* @param {?TraceSegment} [segment]
* The segment to bind the execution of the function to. If omitted or `null`
* the currently active segment will be bound instead.
* @param {boolean} [full]
* Indicates if the full lifetime of the segment is bound to this function.
* @returns {object | Function} The first parameter after wrapping.
*/
function bindSegment(nodule, property, segment, full) {
// Don't bind to null arguments.
if (!nodule) {
return nodule
}
// Determine our arguments.
if (this.isObject(property) && !this.isArray(property)) {
// bindSegment(func, segment [, full])
full = segment
segment = property
property = null
}
// This protects against the `bindSegment(func, null, true)` case, where the
// segment is `null`, and thus `true` (the full param) is detected as the
// segment.
if (segment != null && !this.isObject(segment)) {
this.logger.debug({ segment: segment }, 'Segment is not a segment, not binding.')
return nodule
}
return this.wrap(nodule, property, function wrapFunc(shim, func) {
if (!shim.isFunction(func)) {
return func
}
// Wrap up the function with this segment.
segment = segment || shim.getSegment()
if (!segment) {
return func
}
const binder = _makeBindWrapper(shim, func, segment, full || false)
shim.storeSegment(binder, segment)
return binder
})
}
/**
* Replaces the callback in an arguments array with one that has been bound to
* the given segment.
*
* - `bindCallbackSegment(spec, args, cbIdx [, segment])`
* - `bindCallbackSegment(spec, obj, property [, segment])`
*
* @memberof Shim.prototype
* @param {Spec} spec spec to original wrapped function, used to call after method with arguments passed to callback
* @param {Array | object} args
* The arguments array to pull the cb from.
* @param {number|string} cbIdx
* The index of the callback.
* @param {TraceSegment} [parentSegment]
* The segment to use as the callback segment's parent. Defaults to the
* currently active segment.
* @see Shim#bindSegment
*/
function bindCallbackSegment(spec, args, cbIdx, parentSegment) {
if (!args) {
return
}
if (this.isNumber(cbIdx)) {