1616
1717import { EventEmitter } from 'events' ;
1818import * as shimmer from 'shimmer' ;
19- import { Readable } from 'stream' ;
2019
21- import { Patch , Plugin , Span } from '../plugin-types' ;
20+ import { Patch , Plugin , Span , Tracer } from '../plugin-types' ;
2221
2322import { pg_6 , pg_7 } from './types' ;
2423
2524// TS: Client#query also accepts a callback as a last argument, but TS cannot
2625// detect this as it's a dependent type. So we don't specify it here.
2726type ClientQueryArguments =
28- [ { submit ?: Function } & pg_7 . QueryConfig ] | [ string ] | [ string , { } ] ;
27+ [ Submittable & pg_7 . QueryConfig ] | [ string ] | [ string , { } ] ;
2928type PG7QueryReturnValue = ( pg_7 . QueryConfig & ( { submit : Function } & EventEmitter ) |
3029 pg_7 . Query ) | Promise < pg_7 . QueryResult > ;
31-
32- // tslint:disable-next-line:no-any
33- function isSubmittable ( obj : any ) : obj is { submit : Function } {
34- return typeof obj . submit === 'function' ;
35- }
30+ type Callback < T > = ( err : Error | null , res ?: T ) => void ;
3631
3732const noOp = ( ) => { } ;
3833
@@ -66,46 +61,146 @@ function populateLabelsFromOutputs(
6661 }
6762}
6863
64+ /**
65+ * Partial shape of objects returned by Client#query. Only contains methods that
66+ * are significant to the query lifecycle.
67+ */
68+ type Submittable = {
69+ // Called when the query is completed.
70+ handleReadyForQuery : ( ) => void ;
71+ // Called when an error occurs.
72+ handleError : ( ) => void ;
73+ // A field that is populated when the Submittable is a Query object.
74+ _result ?: pg_7 . QueryResult ;
75+ } ;
76+
77+ /**
78+ * Utility class to help organize patching logic.
79+ */
80+ class PostgresPatchUtility {
81+ readonly maybePopulateLabelsFromInputs : typeof populateLabelsFromInputs ;
82+ readonly maybePopulateLabelsFromOutputs : typeof populateLabelsFromOutputs ;
83+
84+ constructor ( private readonly tracer : Tracer ) {
85+ this . maybePopulateLabelsFromInputs =
86+ tracer . enhancedDatabaseReportingEnabled ( ) ? populateLabelsFromInputs :
87+ noOp ;
88+ this . maybePopulateLabelsFromOutputs =
89+ tracer . enhancedDatabaseReportingEnabled ( ) ? populateLabelsFromOutputs :
90+ noOp ;
91+ }
92+
93+ patchSubmittable ( pgQuery : Submittable , span : Span ) : Submittable {
94+ let spanEnded = false ;
95+ const { maybePopulateLabelsFromOutputs} = this ;
96+ if ( pgQuery . handleError ) {
97+ shimmer . wrap ( pgQuery , 'handleError' , ( origCallback ) => {
98+ // Elements of args are not individually accessed.
99+ // tslint:disable:no-any
100+ return this . tracer . wrap ( function (
101+ this : Submittable , ...args : any [ ] ) : void {
102+ // tslint:enable:no-any
103+ if ( ! spanEnded ) {
104+ const err : Error = args [ 0 ] ;
105+ maybePopulateLabelsFromOutputs ( span , err ) ;
106+ span . endSpan ( ) ;
107+ spanEnded = true ;
108+ }
109+ if ( origCallback ) {
110+ origCallback . apply ( this , args ) ;
111+ }
112+ } ) ;
113+ } ) ;
114+ }
115+ if ( pgQuery . handleReadyForQuery ) {
116+ shimmer . wrap ( pgQuery , 'handleReadyForQuery' , ( origCallback ) => {
117+ // Elements of args are not individually accessed.
118+ // tslint:disable:no-any
119+ return this . tracer . wrap ( function (
120+ this : Submittable , ...args : any [ ] ) : void {
121+ // tslint:enable:no-any
122+ if ( ! spanEnded ) {
123+ maybePopulateLabelsFromOutputs ( span , null , this . _result ) ;
124+ span . endSpan ( ) ;
125+ spanEnded = true ;
126+ }
127+ if ( origCallback ) {
128+ origCallback . apply ( this , args ) ;
129+ }
130+ } ) ;
131+ } ) ;
132+ }
133+ return pgQuery ;
134+ }
135+
136+ patchCallback ( callback : Callback < pg_7 . QueryResult > , span : Span ) :
137+ Callback < pg_7 . QueryResult > {
138+ return this . tracer . wrap ( ( err : Error | null , res ?: pg_7 . QueryResult ) => {
139+ this . maybePopulateLabelsFromOutputs ( span , err , res ) ;
140+ span . endSpan ( ) ;
141+ callback ( err , res ) ;
142+ } ) ;
143+ }
144+
145+ patchPromise ( promise : Promise < pg_7 . QueryResult > , span : Span ) :
146+ Promise < pg_7 . QueryResult > {
147+ return promise = promise . then (
148+ ( res ) => {
149+ this . maybePopulateLabelsFromOutputs ( span , null , res ) ;
150+ span . endSpan ( ) ;
151+ return res ;
152+ } ,
153+ ( err ) => {
154+ this . maybePopulateLabelsFromOutputs ( span , err ) ;
155+ span . endSpan ( ) ;
156+ throw err ;
157+ } ) ;
158+ }
159+ }
160+
69161const plugin : Plugin = [
70162 {
71163 file : 'lib/client.js' ,
72164 versions : '^6.x' ,
73165 // TS: Client is a class name.
74166 // tslint:disable-next-line:variable-name
75167 patch : ( Client , api ) => {
76- const maybePopulateLabelsFromInputs =
77- api . enhancedDatabaseReportingEnabled ( ) ? populateLabelsFromInputs :
78- noOp ;
79- const maybePopulateLabelsFromOutputs =
80- api . enhancedDatabaseReportingEnabled ( ) ? populateLabelsFromOutputs :
81- noOp ;
168+ const pgPatch = new PostgresPatchUtility ( api ) ;
169+
82170 shimmer . wrap ( Client . prototype , 'query' , ( query ) => {
83- return function query_trace ( this : pg_6 . Client ) {
84- const span = api . createChildSpan ( { name : 'pg-query' } ) ;
85- if ( ! api . isRealSpan ( span ) ) {
86- return query . apply ( this , arguments ) ;
87- }
88- const argLength = arguments . length ;
89- if ( argLength >= 1 ) {
90- const args : ClientQueryArguments =
91- Array . prototype . slice . call ( arguments , 0 ) ;
171+ // Every call to Client#query will have a Submittable object associated
172+ // with it. We need to patch two handlers (handleReadyForQuery and
173+ // handleError) to end a span.
174+ // There are a few things to note here:
175+ // * query accepts a Submittable or a string. A Query is a Submittable.
176+ // So if we can get a Submittable from the input we patch it
177+ // proactively, otherwise (in the case of a string) we patch the
178+ // output Query instead.
179+ // * If query is passed a callback, the callback will be invoked from
180+ // either handleReadyForQuery or handleError. So we don't need to
181+ // separately patch the callback.
182+ return function query_trace (
183+ this : pg_6 . Client , ...args : ClientQueryArguments ) {
184+ if ( args . length >= 1 ) {
185+ const span = api . createChildSpan ( { name : 'pg-query' } ) ;
186+ if ( ! api . isRealSpan ( span ) ) {
187+ return query . apply ( this , args ) ;
188+ }
92189 // Extract query text and values, if needed.
93- maybePopulateLabelsFromInputs ( span , args ) ;
190+ pgPatch . maybePopulateLabelsFromInputs ( span , args ) ;
191+ if ( typeof args [ 0 ] === 'object' ) {
192+ pgPatch . patchSubmittable ( args [ 0 ] , span ) ;
193+ return query . apply ( this , args ) ;
194+ } else {
195+ return pgPatch . patchSubmittable (
196+ query . apply ( this , args ) as Submittable , span ) ;
197+ }
198+ } else {
199+ // query was called with no arguments.
200+ // This doesn't make sense, but don't do anything that might cause
201+ // an error to get thrown here, or a span to be started.
202+ return query . apply ( this , args ) ;
94203 }
95- const pgQuery : pg_6 . QueryReturnValue = query . apply ( this , arguments ) ;
96- api . wrapEmitter ( pgQuery ) ;
97- const done = pgQuery . callback ;
98- // TODO(kjin): Clean up this line a little bit by casting the function
99- // passed to api.wrap as a NonNullable<typeof done>.
100- pgQuery . callback =
101- api . wrap ( ( err : Error | null , res ?: pg_7 . QueryResult ) => {
102- maybePopulateLabelsFromOutputs ( span , err , res ) ;
103- span . endSpan ( ) ;
104- if ( done ) {
105- done ( err , res ) ;
106- }
107- } ) ;
108- return pgQuery ;
109204 } ;
110205 } ) ;
111206 } ,
@@ -121,12 +216,7 @@ const plugin: Plugin = [
121216 // TS: Client is a class name.
122217 // tslint:disable-next-line:variable-name
123218 patch : ( Client , api ) => {
124- const maybePopulateLabelsFromInputs =
125- api . enhancedDatabaseReportingEnabled ( ) ? populateLabelsFromInputs :
126- noOp ;
127- const maybePopulateLabelsFromOutputs =
128- api . enhancedDatabaseReportingEnabled ( ) ? populateLabelsFromOutputs :
129- noOp ;
219+ const pgPatch = new PostgresPatchUtility ( api ) ;
130220 shimmer . wrap ( Client . prototype , 'query' , ( query ) => {
131221 return function query_trace ( this : pg_7 . Client ) {
132222 const span = api . createChildSpan ( { name : 'pg-query' } ) ;
@@ -151,25 +241,18 @@ const plugin: Plugin = [
151241 Array . prototype . slice . call ( arguments , 0 ) ;
152242
153243 // Extract query text and values, if needed.
154- maybePopulateLabelsFromInputs ( span , args ) ;
244+ pgPatch . maybePopulateLabelsFromInputs ( span , args ) ;
155245
156246 // If we received a callback, bind it to the current context,
157247 // optionally adding labels as well.
158248 const callback = args [ args . length - 1 ] ;
159249 if ( typeof callback === 'function' ) {
160- args [ args . length - 1 ] =
161- api . wrap ( ( err : Error | null , res ?: pg_7 . QueryResult ) => {
162- maybePopulateLabelsFromOutputs ( span , err , res ) ;
163- span . endSpan ( ) ;
164- // TS: Type cast is safe as we know that callback is a
165- // Function.
166- ( callback as ( err : Error | null , res ?: pg_7 . QueryResult ) =>
167- void ) ( err , res ) ;
168- } ) ;
169- pgQuery = query . apply ( this , args ) ;
170- } else {
171- pgQuery = query . apply ( this , arguments ) ;
250+ args [ args . length - 1 ] = pgPatch . patchCallback (
251+ callback as Callback < pg_7 . QueryResult > , span ) ;
252+ } else if ( typeof args [ 0 ] === 'object' ) {
253+ pgPatch . patchSubmittable ( args [ 0 ] as Submittable , span ) ;
172254 }
255+ pgQuery = query . apply ( this , args ) ;
173256 } else {
174257 pgQuery = query . apply ( this , arguments ) ;
175258 }
@@ -178,19 +261,10 @@ const plugin: Plugin = [
178261 if ( pgQuery instanceof EventEmitter ) {
179262 api . wrapEmitter ( pgQuery ) ;
180263 } else if ( typeof pgQuery . then === 'function' ) {
181- // Ensure that the span is ended, optionally adding labels as
182- // well.
183- pgQuery = pgQuery . then (
184- ( res ) => {
185- maybePopulateLabelsFromOutputs ( span , null , res ) ;
186- span . endSpan ( ) ;
187- return res ;
188- } ,
189- ( err ) => {
190- maybePopulateLabelsFromOutputs ( span , err ) ;
191- span . endSpan ( ) ;
192- throw err ;
193- } ) ;
264+ // Unlike in pg 6, the returned value can't be both a Promise and
265+ // a Submittable. So we don't run the risk of double-patching
266+ // here.
267+ pgPatch . patchPromise ( pgQuery , span ) ;
194268 }
195269 }
196270 return pgQuery ;
0 commit comments