/**
 * Copyright 2018, OpenCensus Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import { BasePlugin , Func , HeaderGetter , HeaderSetter , Span , TraceOptions , MessageEventType , SpanKind , CanonicalCode } from '@opencensus/core' ; import * as httpModule from 'http' ; import * as semver from 'semver' ; import * as shimmer from 'shimmer' ; import * as url from 'url' ; import * as uuid from 'uuid' ; import { kMiddlewareStack } from './express' ; export type IgnoreMatcher < T > = string | RegExp | ( ( url : string , request : T ) => boolean ) ; export type HttpPluginConfig = { /**
     * Ignore specific incoming request depending on their path
     */ ignoreIncomingPaths : Array < IgnoreMatcher < httpModule . IncomingMessage > > ; /**
     * Ignore specific outgoing request depending on their url
     */ ignoreOutgoingUrls : Array < IgnoreMatcher < httpModule . ClientRequest > > ; /**
     * Disable the creation of root span with http server
     * mainly used if net plugin is implemented
     */ createSpanWithNet : boolean ; } ; export type HttpModule = typeof httpModule ; export type RequestFunction = typeof httpModule . request ; /** Http instrumentation plugin for Opencensus */ export class HttpPlugin extends BasePlugin { /**
     * Attributes Names according to Opencensus HTTP Specs
     * https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/HTTP.md
     */ static ATTRIBUTE_HTTP_HOST = 'http.host' ; static ATTRIBUTE_HTTP_METHOD = 'http.method' ; static ATTRIBUTE_HTTP_PATH = 'http.path' ; static ATTRIBUTE_HTTP_ROUTE = 'http.route' ; static ATTRIBUTE_HTTP_USER_AGENT = 'http.user_agent' ; static ATTRIBUTE_HTTP_STATUS_CODE = 'http.status_code' ; static ATTRIBUTE_HTTP_ERROR_NAME = 'http.error_name' ; static ATTRIBUTE_HTTP_ERROR_MESSAGE = 'http.error_message' ; protected options : HttpPluginConfig ; /** Constructs a new HttpPlugin instance. */ constructor ( moduleName ) { super ( moduleName ) ; } /**
     * Patches HTTP incoming and outcoming request functions.
     */ protected applyPatch ( ) { this . logger . debug ( 'applying patch to %s@%s' , this . moduleName , this . version ) ; shimmer . wrap ( this . moduleExports , 'request' , this . getPatchOutgoingRequestFunction ( ) ) ; if ( semver . satisfies ( this . version , '>=8.0.0' ) ) { shimmer . wrap ( this . moduleExports , 'get' , ( ) => { return function getTrace ( options , callback ) { const req = httpModule . request ( options , callback ) ; req . end ( ) ; return req ; } ; } ) ; } if ( this . moduleExports && this . moduleExports . Server && this . moduleExports . Server . prototype ) { shimmer . wrap ( this . moduleExports . Server . prototype , 'emit' , this . getPatchIncomingRequestFunction ( ) ) ; } else { this . logger . error ( 'Could not apply patch to %s.emit. Interface is not as expected.' , this . moduleName ) ; } return this . moduleExports ; } /** Unpatches all HTTP patched function. */ protected applyUnpatch ( ) { shimmer . unwrap ( this . moduleExports , 'request' ) ; if ( semver . satisfies ( this . version , '>=8.0.0' ) ) { shimmer . unwrap ( this . moduleExports , 'get' ) ; } if ( this . moduleExports && this . moduleExports . Server && this . moduleExports . Server . prototype ) { shimmer . unwrap ( this . moduleExports . Server . prototype , 'emit' ) ; } } param url param request param list protected isIgnored < T > ( url , request , list ) { if ( ! list ) { return false ; } for ( const pattern of list ) { if ( this . isSatisfyPattern ( url , request , pattern ) ) { return true ; } } return false ; } param url param request param pattern protected isSatisfyPattern < T > ( url , request , pattern ) { if ( typeof pattern === 'string' ) { return pattern === url ; } else if ( pattern instanceof RegExp ) { return pattern . test ( url ) ; } else if ( typeof pattern === 'function' ) { return pattern ( url , request ) ; } else { throw new TypeError ( 'Pattern is in unsupported datatype' ) ; } } /**
     * Creates spans for incoming requests, restoring spans' context if applied.
     */ protected getPatchIncomingRequestFunction ( ) { return ( original ) => { const plugin = this ; return function incomingRequest ( event , ... args ) : boolean { if ( event !== 'request' ) { return original . apply ( this , arguments ) ; } const request = args [ 0 ] ; const response = args [ 1 ] ; const path = url . parse ( request . url ) . pathname ; plugin . logger . debug ( '%s plugin incomingRequest' , plugin . moduleName ) ; if ( plugin . isIgnored ( path , request , plugin . options . ignoreIncomingPaths ) ) { return original . apply ( this , arguments ) ; } const propagation = plugin . tracer . propagation ; const headers = request . headers ; const getter = { getHeader ( name ) { return headers [ name ] ; } } ; const context = propagation ? propagation . extract ( getter ) : null ; const traceOptions = { name : path , kind : SpanKind . SERVER , spanContext : context !== null ? context : undefined } ; return plugin . createSpan ( traceOptions , rootSpan => { if ( ! rootSpan ) return original . apply ( this , arguments ) ; plugin . tracer . wrapEmitter ( request ) ; plugin . tracer . wrapEmitter ( response ) ; const originalEnd = response . end ; response . end = function ( this ) { response . end = originalEnd ; const returned = response . end . apply ( this , arguments ) ; const requestUrl = url . parse ( request . url || 'localhost' ) ; const host = headers . host || 'localhost' ; const userAgent = ( headers [ 'user-agent' ] || headers [ 'User-Agent' ] ) as string ; rootSpan . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_HOST , host . replace ( /^(.*)(\:[0-9]{1,5})/ , '$1' ) ) ; rootSpan . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_METHOD , request . method || 'GET' ) ; rootSpan . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_PATH , `${ requestUrl . pathname }` ) ; let route = `${ requestUrl . path }` ; const middlewareStack = request [ kMiddlewareStack ] ; if ( middlewareStack ) { route = middlewareStack . filter ( path => path !== '/' ) . map ( path => { return path [ 0 ] === '/' ? path : '/' + path ; } ) . join ( '' ) ; } rootSpan . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_ROUTE , route ) ; rootSpan . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_USER_AGENT , userAgent ) ; rootSpan . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_STATUS_CODE , response . statusCode . toString ( ) ) ; rootSpan . setStatus ( HttpPlugin . convertTraceStatus ( response . statusCode ) ) ; rootSpan . addMessageEvent ( MessageEventType . RECEIVED , uuid . v4 ( ) . split ( '-' ) . join ( '' ) ) ; rootSpan . end ( ) ; return returned ; } ; return original . apply ( this , arguments ) ; } ) ; } ; } ; } /**
     * Creates spans for outgoing requests, sending spans' context for distributed
     * tracing.
     */ protected getPatchOutgoingRequestFunction ( ) { return ( original ) : Func < httpModule . ClientRequest > => { const plugin = this ; const kind = plugin . moduleName === 'https' ? 'HTTPS' : 'HTTP' ; return function outgoingRequest ( options , callback ) : httpModule . ClientRequest { if ( ! options ) { return original . apply ( this , arguments ) ; } let pathname = '' ; let method = 'GET' ; let origin = '' ; if ( typeof ( options ) === 'string' ) { const parsedUrl = url . parse ( options ) ; options = parsedUrl ; pathname = parsedUrl . pathname || '/' ; origin = `${ parsedUrl . protocol || 'http:' }//${ parsedUrl . host }` ; } else { if ( options . headers && options . headers [ 'x-opencensus-outgoing-request' ] ) { plugin . logger . debug ( 'header with "x-opencensus-outgoing-request" - do not trace' ) ; return original . apply ( this , arguments ) ; } try { pathname = ( options as url . URL ) . pathname || '' ; if ( pathname . length === 0 && typeof options . path === 'string' ) { pathname = url . parse ( options . path ) . pathname || '' ; } method = options . method || 'GET' ; origin = `${ options . protocol || 'http:' }//${ options . host }` ; } catch ( e ) { return original . apply ( this , arguments ) ; } } const request = original . apply ( this , arguments ) ; if ( plugin . isIgnored ( origin + pathname , request , plugin . options . ignoreOutgoingUrls ) ) { return request ; } plugin . tracer . wrapEmitter ( request ) ; plugin . logger . debug ( '%s plugin outgoingRequest' , plugin . moduleName ) ; const traceOptions = { name : `${ kind . toLowerCase ( ) }-${ ( method || 'GET' ) . toLowerCase ( ) }` , kind : SpanKind . CLIENT } ; if ( ! plugin . tracer . currentRootSpan ) { plugin . logger . debug ( 'outgoingRequest starting a root span' ) ; return plugin . tracer . startRootSpan ( traceOptions , plugin . getMakeRequestTraceFunction ( request , options , plugin ) ) ; } else { plugin . logger . debug ( 'outgoingRequest starting a child span' ) ; const span = plugin . tracer . startChildSpan ( traceOptions . name , traceOptions . kind ) ; return ( plugin . getMakeRequestTraceFunction ( request , options , plugin ) ) ( span ) ; } } ; } ; } param original param options private getMakeRequestTraceFunction ( request , options , plugin ) { return ( span ) : httpModule . ClientRequest => { plugin . logger . debug ( 'makeRequestTrace' ) ; if ( ! span ) { plugin . logger . debug ( 'makeRequestTrace span is null' ) ; return request ; } const setter = { setHeader ( name , value ) { if ( plugin . hasExpectHeader ( options ) && options . headers ) { if ( options . __cloned !== true ) { options = Object . assign ( { } , options ) as httpModule . ClientRequestArgs ; options . headers = Object . assign ( { } , options . headers ) ; options . __cloned = true ; } options . headers [ name ] = value ; } else { request . setHeader ( name , value ) ; } } } ; const propagation = plugin . tracer . propagation ; if ( propagation ) { propagation . inject ( setter , span . spanContext ) ; } request . on ( 'response' , ( response ) => { plugin . tracer . wrapEmitter ( response ) ; plugin . logger . debug ( 'outgoingRequest on response()' ) ; response . on ( 'end' , ( ) => { plugin . logger . debug ( 'outgoingRequest on end()' ) ; const method = response . method ? response . method : 'GET' ; const headers = options . headers ; const userAgent = headers ? ( headers [ 'user-agent' ] || headers [ 'User-Agent' ] ) : null ; if ( options . host || options . hostname ) { const value = options . host || options . hostname ; span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_HOST , `${ value }` ) ; } span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_METHOD , method ) ; span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_PATH , `${ options . path }` ) ; span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_ROUTE , `${ options . path }` ) ; if ( userAgent ) { span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_USER_AGENT , userAgent . toString ( ) ) ; } span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_STATUS_CODE , `${ response . statusCode }` ) ; span . setStatus ( HttpPlugin . convertTraceStatus ( response . statusCode || 0 ) ) ; span . addMessageEvent ( MessageEventType . SENT , uuid . v4 ( ) . split ( '-' ) . join ( '' ) ) ; span . end ( ) ; } ) ; response . on ( 'error' , error => { span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_ERROR_NAME , error . name ) ; span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_ERROR_MESSAGE , error . message ) ; span . setStatus ( CanonicalCode . UNKNOWN ) ; span . end ( ) ; } ) ; } ) ; request . on ( 'error' , error => { span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_ERROR_NAME , error . name ) ; span . addAttribute ( HttpPlugin . ATTRIBUTE_HTTP_ERROR_MESSAGE , error . message ) ; span . setStatus ( CanonicalCode . UNKNOWN ) ; span . end ( ) ; } ) ; plugin . logger . debug ( 'makeRequestTrace return request' ) ; return request ; } ; } private createSpan < T > ( options , fn ) { const forceChildspan = this . options . createSpanWithNet === true ; if ( forceChildspan ) { const span = this . tracer . startChildSpan ( options . name , options . kind ) ; return fn ( span ) ; } else { return this . tracer . startRootSpan ( options , fn ) ; } } param statusCode static convertTraceStatus ( statusCode ) { if ( statusCode < 200 || statusCode > 504 ) { return TraceStatusCodes . UNKNOWN ; } else if ( statusCode >= 200 && statusCode < 400 ) { return TraceStatusCodes . OK ; } else { switch ( statusCode ) { case ( 400 ) : return TraceStatusCodes . INVALID_ARGUMENT ; case ( 504 ) : return TraceStatusCodes . DEADLINE_EXCEEDED ; case ( 404 ) : return TraceStatusCodes . NOT_FOUND ; case ( 403 ) : return TraceStatusCodes . PERMISSION_DENIED ; case ( 401 ) : return TraceStatusCodes . UNAUTHENTICATED ; case ( 429 ) : return TraceStatusCodes . RESOURCE_EXHAUSTED ; case ( 501 ) : return TraceStatusCodes . UNIMPLEMENTED ; case ( 503 ) : return TraceStatusCodes . UNAVAILABLE ; default : return TraceStatusCodes . UNKNOWN ; } } } param options hasExpectHeader ( options ) { return ! ! ( ( options as httpModule . ClientRequestArgs ) . headers && ( options as httpModule . ClientRequestArgs ) . headers ! . Expect ) ; } } /**
 * An enumeration of OpenCensus Trace status codes.
 */ export enum TraceStatusCodes { UNKNOWN = 2 , OK = 0 , INVALID_ARGUMENT = 3 , DEADLINE_EXCEEDED = 4 , NOT_FOUND = 5 , PERMISSION_DENIED = 7 , UNAUTHENTICATED = 16 , RESOURCE_EXHAUSTED = 8 , UNIMPLEMENTED = 12 , UNAVAILABLE = 14 } export const plugin = new HttpPlugin ( 'http' ) ;