11/**
2- * Copyright 2018 Google Inc. All Rights Reserved.
2+ * Copyright 2018, OpenCensus Authors
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1414 * limitations under the License.
1515 */
1616
17- import * as semver from 'semver'
18- import * as url from 'url'
19- import * as eos from 'end-of-stream'
17+ import { types } from '@opencensus/opencensus-core' ;
18+ import { classes } from '@opencensus/opencensus-core' ;
19+ import { logger } from '@opencensus/opencensus-core' ;
20+ import { B3Format } from '@opencensus/opencensus-propagation-b3' ;
21+ import * as semver from 'semver' ;
22+ import * as shimmer from 'shimmer' ;
23+ import * as url from 'url' ;
24+ import * as uuid from 'uuid' ;
2025
21- import { Tracer } from '@opencensus/opencensus-core'
22- import { debug } from '@opencensus/opencensus-core'
23- import { Plugin , BasePlugin } from '@opencensus/opencensus-core'
24- import { TraceOptions , TraceContext } from '@opencensus/opencensus-core' ;
25- import { B3Format } from '@opencensus/opencensus-propagation-b3'
26+ // import * as Debug from 'debug';
27+ // const debug = Debug('opencensus');
2628
27- export class HttpPlugin extends BasePlugin implements Plugin {
29+ export class HttpPlugin extends classes . BasePlugin {
30+ logger : types . Logger ;
2831
29- constructor ( ) {
30- super ( 'http' ) ;
31- }
32+ /** Constructs a new HttpPlugin instance. */
33+ constructor ( moduleName : string ) {
34+ super ( moduleName ) ;
35+ }
3236
33- applyPatch ( exporter : any , tracer : Tracer , version : string ) {
34- this . setPluginContext ( exporter , tracer , version ) ;
35- return exporter
36- }
37+ /**
38+ * Patches HTTP incoming and outcoming request functions.
39+ * @param moduleExporters The HTTP package.
40+ * @param tracer A tracer instance to create spans on.
41+ * @param version The package version.
42+ */
43+ // tslint:disable:no-any
44+ applyPatch ( moduleExporters : any , tracer : types . Tracer , version : string ) {
45+ this . setPluginContext ( moduleExporters , tracer , version ) ;
46+ this . logger = tracer . logger || logger . logger ( 'debug' ) ;
3747
38- applyUnpatch ( ) : void {
48+ shimmer . wrap ( moduleExporters , 'request' , this . patchOutgoingRequest ( ) ) ;
3949
50+ // In Node 8, http.get calls a private request method, therefore we patch it
51+ // here too.
52+ if ( semver . satisfies ( version , '>=8.0.0' ) ) {
53+ shimmer . wrap ( moduleExporters , 'get' , this . patchOutgoingRequest ( ) ) ;
4054 }
4155
56+ shimmer . wrap (
57+ moduleExporters && moduleExporters . Server &&
58+ moduleExporters . Server . prototype ,
59+ 'emit' , this . patchIncomingRequest ( ) ) ;
60+
61+ return moduleExporters ;
62+ }
63+
64+ /** Unpatches all HTTP patched function. */
65+ applyUnpatch ( ) : void {
66+ shimmer . unwrap ( this . moduleExporters , 'request' ) ;
67+ shimmer . unwrap ( this . moduleExporters , 'get' ) ;
68+ shimmer . unwrap (
69+ this . moduleExporters && this . moduleExporters . Server &&
70+ this . moduleExporters . Server . prototype ,
71+ 'emit' ) ;
72+ }
73+
74+ /**
75+ * Creates spans for incoming requests, restoring spans' context if applied.
76+ * @param plugin The HTTP plugin itself.
77+ */
78+ patchIncomingRequest ( ) {
79+ const plugin : HttpPlugin = this ;
80+ return ( original : Function ) : Function => {
81+ // tslint:disable:no-any
82+ return function incomingRequest (
83+ event : string , request : any , response : any ) {
84+ // Only traces request events
85+ if ( event !== 'request' ) {
86+ return original . apply ( this , arguments ) ;
87+ }
88+
89+ const traceOptions = {
90+ name : arguments [ 1 ] . url ,
91+ type : 'SERVER' ,
92+ spanContext : B3Format . extractFromHeader ( arguments [ 1 ] . headers )
93+ } ;
94+
95+ return plugin . tracer . startRootSpan ( traceOptions , rootSpan => {
96+ if ( ! rootSpan ) return original . apply ( this , arguments ) ;
97+
98+ plugin . tracer . wrapEmitter ( request ) ;
99+ plugin . tracer . wrapEmitter ( response ) ;
100+
101+ // Wraps end (inspired by:
102+ // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75)
103+ const originalEnd = response . end ;
104+ // tslint:disable:no-any
105+ response . end = function ( this : any ) {
106+ response . end = originalEnd ;
107+ const returned = response . end . apply ( this , arguments ) ;
108+
109+ rootSpan . addAttribute ( 'http.host' , request . url . host ) ;
110+ rootSpan . addAttribute ( 'http.method' , request . method ) ;
111+ rootSpan . addAttribute ( 'http.path' , request . url . pathname ) ;
112+ rootSpan . addAttribute ( 'http.route' , request . url . path ) ;
113+ rootSpan . addAttribute (
114+ 'http.user_agent' , 'HTTPClient/' + request . httpVersion ) ;
115+ rootSpan . addAttribute ( 'http.status_code' , request . statusCode ) ;
116+
117+ rootSpan . status = plugin . traceStatus ( request . statusCode ) ;
118+
119+ // Message Event ID is not defined
120+ rootSpan . addMessageEvent (
121+ 'MessageEventTypeRecv' , uuid . v4 ( ) . split ( '-' ) . join ( '' ) ) ;
122+
123+ // debug('Ended root span: ', rootSpan.name, rootSpan.id,
124+ // rootSpan.traceId);
125+ rootSpan . end ( ) ;
126+ return returned ;
127+ } ;
128+
129+ return original . apply ( this , arguments ) ;
130+ } ) ;
131+ } ;
132+ } ;
133+ }
134+
135+ /**
136+ * Creates spans for outgoing requests, sending spans' context for distributed
137+ * tracing.
138+ * @param plugin The HTTP plugin itself.
139+ */
140+ patchOutgoingRequest ( ) {
141+ const plugin : HttpPlugin = this ;
142+ // tslint:disable:no-any
143+ return ( original : any ) : any => {
144+ return function outgoingRequest ( ) {
145+ // Makes sure the url is an url object
146+ if ( typeof ( arguments [ 0 ] ) === 'string' ) {
147+ arguments [ 0 ] = url . parse ( arguments [ 0 ] ) ;
148+ }
149+
150+ // Do not trace ourselves
151+ if ( arguments [ 0 ] . headers &&
152+ arguments [ 0 ] . headers [ 'x-opencensus-outgoing-request' ] ) {
153+ // tslint:disable:no-any
154+ return original . apply ( this as any , arguments ) ;
155+ }
156+
157+ const traceOptions = {
158+ name : arguments [ 0 ] . pathname ,
159+ type : 'CLIENT' ,
160+ } ;
161+
162+ // Checks if this outgoing request is part of an operation by checking
163+ // if there is a current root span, if so, we create a child span. In
164+ // case there is no root span, this means that the outgoing request is
165+ // the first operation, therefore we create a root span.
166+ if ( ! plugin . tracer . currentRootSpan ) {
167+ return plugin . tracer . startRootSpan (
168+ traceOptions ,
169+ plugin . makeRequestTrace ( original , arguments , plugin ) ) ;
170+ } else {
171+ const span : types . Span = plugin . tracer . startChildSpan (
172+ traceOptions . name , traceOptions . type ) ;
173+ return ( span : types . Span ) =>
174+ plugin . makeRequestTrace ( original , arguments , plugin ) ;
175+ }
176+ } ;
177+ } ;
178+ }
179+
180+ /**
181+ * Injects span's context to header for distributed tracing and finshes the
182+ * span when the response is finished.
183+ * @param original The original patched function.
184+ * @param args The arguments to the original function.
185+ */
186+ // tslint:disable:no-any
187+ makeRequestTrace ( original : Function , args : any , plugin : HttpPlugin ) : any {
188+ return ( span : types . Span ) => {
189+ args [ 0 ] . headers = B3Format . injectToHeader ( args [ 0 ] . headers , span ) ;
190+
191+ const request = original . apply ( this , args ) ;
192+
193+ if ( ! span ) {
194+ return request ;
195+ }
196+
197+ // tslint:disable:no-any
198+ request . on ( 'response' , ( response : any ) => {
199+ response . on ( 'end' , ( ) => {
200+ span . addAttribute ( 'http.host' , args [ 0 ] . host ) ;
201+ span . addAttribute ( 'http.method' , request . method ) ;
202+ span . addAttribute ( 'http.path' , args [ 0 ] . pathname ) ;
203+ span . addAttribute ( 'http.route' , args [ 0 ] . path ) ;
204+ span . addAttribute (
205+ 'http.user_agent' , 'HTTPClient/' + request . httpVersion ) ;
206+ span . addAttribute ( 'http.status_code' , request . statusCode ) ;
207+
208+ span . status = plugin . traceStatus ( request . statusCode ) ;
209+
210+ // Message Event ID is not defined
211+ span . addMessageEvent (
212+ 'MessageEventTypeSent' , uuid . v4 ( ) . split ( '-' ) . join ( '' ) ) ;
213+
214+ // debug('Ended span: ', span.name, span.id, span.traceId);
215+ span . end ( ) ;
216+ } ) ;
217+ } ) ;
218+
219+ return request ;
220+ } ;
221+ }
222+
223+ traceStatus ( statusCode : number ) : number {
224+ if ( statusCode < 200 || statusCode >= 400 ) {
225+ return traceStatusCodes . UNKNOWN ;
226+ } else {
227+ switch ( statusCode ) {
228+ case ( 400 ) :
229+ return traceStatusCodes . INVALID_ARGUMENT ;
230+ case ( 504 ) :
231+ return traceStatusCodes . DEADLINE_EXCEEDED ;
232+ case ( 404 ) :
233+ return traceStatusCodes . NOT_FOUND ;
234+ case ( 403 ) :
235+ return traceStatusCodes . PERMISSION_DENIED ;
236+ case ( 401 ) :
237+ return traceStatusCodes . UNAUTHENTICATED ;
238+ case ( 429 ) :
239+ return traceStatusCodes . RESOURCE_EXHAUSTED ;
240+ case ( 501 ) :
241+ return traceStatusCodes . UNIMPLEMENTED ;
242+ case ( 503 ) :
243+ return traceStatusCodes . UNAVAILABLE ;
244+ default :
245+ return traceStatusCodes . OK ;
246+ }
247+ }
248+ }
249+ }
42250
251+ enum traceStatusCodes {
252+ UNKNOWN = 2 ,
253+ OK = 0 ,
254+ INVALID_ARGUMENT = 3 ,
255+ DEADLINE_EXCEEDED = 4 ,
256+ NOT_FOUND = 5 ,
257+ PERMISSION_DENIED = 7 ,
258+ UNAUTHENTICATED = 16 ,
259+ RESOURCE_EXHAUSTED = 8 ,
260+ UNIMPLEMENTED = 12 ,
261+ UNAVAILABLE = 14
43262}
44263
45- module . exports = new HttpPlugin ( ) ;
264+ const plugin = new HttpPlugin ( 'http' ) ;
265+ export { plugin } ;
0 commit comments