2024-08-08 10:58:05 +08:00
/ *
* Copyright The OpenTelemetry 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
*
* https : //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 { SpanKind , Span , context , propagation } from '@opentelemetry/api' ;
import {
HTTPFLAVORVALUES_HTTP_1_1 ,
NETTRANSPORTVALUES_IP_TCP ,
SEMATTRS_HTTP_FLAVOR ,
SEMATTRS_HTTP_HOST ,
SEMATTRS_NET_TRANSPORT ,
} from '@opentelemetry/semantic-conventions' ;
import * as assert from 'assert' ;
import * as url from 'url' ;
import { HttpInstrumentation } from '../../src/http' ;
import { assertSpan } from '../utils/assertSpan' ;
import * as utils from '../utils/utils' ;
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' ;
import {
InMemorySpanExporter ,
SimpleSpanProcessor ,
} from '@opentelemetry/sdk-trace-base' ;
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks' ;
const instrumentation = new HttpInstrumentation ( ) ;
instrumentation . enable ( ) ;
instrumentation . disable ( ) ;
import * as http from 'http' ;
import { httpRequest } from '../utils/httpRequest' ;
import { DummyPropagation } from '../utils/DummyPropagation' ;
import { Socket } from 'net' ;
import { sendRequestTwice } from '../utils/rawRequest' ;
const protocol = 'http' ;
const serverPort = 32345 ;
const hostname = 'localhost' ;
const memoryExporter = new InMemorySpanExporter ( ) ;
const customAttributeFunction = ( span : Span ) : void = > {
span . setAttribute ( 'span kind' , SpanKind . CLIENT ) ;
} ;
describe ( 'HttpInstrumentation Integration tests' , ( ) = > {
let mockServerPort = 0 ;
let mockServer : http.Server ;
const sockets : Array < Socket > = [ ] ;
before ( done = > {
mockServer = http . createServer ( ( req , res ) = > {
if ( req . url === '/timeout' ) {
setTimeout ( ( ) = > {
res . end ( ) ;
} , 1000 ) ;
}
res . statusCode = 200 ;
res . setHeader ( 'content-type' , 'application/json' ) ;
res . write (
JSON . stringify ( {
success : true ,
} )
) ;
res . end ( ) ;
} ) ;
mockServer . listen ( 0 , ( ) = > {
const addr = mockServer . address ( ) ;
if ( addr == null ) {
done ( new Error ( 'unexpected addr null' ) ) ;
return ;
}
if ( typeof addr === 'string' ) {
done ( new Error ( ` unexpected addr ${ addr } ` ) ) ;
return ;
}
if ( addr . port <= 0 ) {
done ( new Error ( 'Could not get port' ) ) ;
return ;
}
mockServerPort = addr . port ;
done ( ) ;
} ) ;
} ) ;
after ( done = > {
sockets . forEach ( s = > s . destroy ( ) ) ;
mockServer . close ( done ) ;
} ) ;
beforeEach ( ( ) = > {
memoryExporter . reset ( ) ;
} ) ;
before ( ( ) = > {
propagation . setGlobalPropagator ( new DummyPropagation ( ) ) ;
context . setGlobalContextManager ( new AsyncHooksContextManager ( ) . enable ( ) ) ;
} ) ;
after ( ( ) = > {
context . disable ( ) ;
propagation . disable ( ) ;
} ) ;
describe ( 'enable()' , ( ) = > {
before ( function ( done ) {
// mandatory
if ( process . env . CI ) {
done ( ) ;
return ;
}
utils . checkInternet ( isConnected = > {
if ( ! isConnected ) {
this . skip ( ) ;
// don't disturb people
}
done ( ) ;
} ) ;
} ) ;
const provider = new NodeTracerProvider ( ) ;
provider . addSpanProcessor ( new SimpleSpanProcessor ( memoryExporter ) ) ;
instrumentation . setTracerProvider ( provider ) ;
beforeEach ( ( ) = > {
memoryExporter . reset ( ) ;
} ) ;
before ( ( ) = > {
const ignoreConfig = [
` ${ protocol } :// ${ hostname } : ${ serverPort } /ignored/string ` ,
/\/ignored\/regexp$/i ,
( url : string ) = > url . endsWith ( '/ignored/function' ) ,
] ;
instrumentation . setConfig ( {
ignoreIncomingPaths : ignoreConfig ,
ignoreOutgoingUrls : ignoreConfig ,
applyCustomAttributesOnSpan : customAttributeFunction ,
} ) ;
instrumentation . enable ( ) ;
} ) ;
after ( ( ) = > {
instrumentation . disable ( ) ;
} ) ;
it ( 'should create a rootSpan for GET requests and add propagation headers' , async ( ) = > {
let spans = memoryExporter . getFinishedSpans ( ) ;
assert . strictEqual ( spans . length , 0 ) ;
const result = await httpRequest . get (
` ${ protocol } ://localhost: ${ mockServerPort } /?query=test `
) ;
spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( s = > s . kind === SpanKind . CLIENT ) ;
assert . ok ( span ) ;
const validations = {
hostname : 'localhost' ,
httpStatusCode : result.statusCode ! ,
httpMethod : 'GET' ,
pathname : '/' ,
path : '/?query=test' ,
resHeaders : result.resHeaders ,
reqHeaders : result.reqHeaders ,
component : 'http' ,
} ;
assert . strictEqual ( spans . length , 2 ) ;
assert . strictEqual ( span . name , 'GET' ) ;
assertSpan ( span , SpanKind . CLIENT , validations ) ;
} ) ;
it ( 'should create a rootSpan for GET requests and add propagation headers if URL is used' , async ( ) = > {
let spans = memoryExporter . getFinishedSpans ( ) ;
assert . strictEqual ( spans . length , 0 ) ;
const result = await httpRequest . get (
new url . URL ( ` ${ protocol } ://localhost: ${ mockServerPort } /?query=test ` )
) ;
spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( s = > s . kind === SpanKind . CLIENT ) ;
assert . ok ( span ) ;
const validations = {
hostname : 'localhost' ,
httpStatusCode : result.statusCode ! ,
httpMethod : 'GET' ,
pathname : '/' ,
path : '/?query=test' ,
resHeaders : result.resHeaders ,
reqHeaders : result.reqHeaders ,
component : 'http' ,
} ;
assert . strictEqual ( spans . length , 2 ) ;
assert . strictEqual ( span . name , 'GET' ) ;
assertSpan ( span , SpanKind . CLIENT , validations ) ;
} ) ;
it ( 'should create a valid rootSpan with propagation headers for GET requests if URL and options are used' , async ( ) = > {
let spans = memoryExporter . getFinishedSpans ( ) ;
assert . strictEqual ( spans . length , 0 ) ;
const result = await httpRequest . get (
new url . URL ( ` ${ protocol } ://localhost: ${ mockServerPort } /?query=test ` ) ,
{
headers : { 'x-foo' : 'foo' } ,
}
) ;
spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( s = > s . kind === SpanKind . CLIENT ) ;
assert . ok ( span ) ;
const validations = {
hostname : 'localhost' ,
httpStatusCode : result.statusCode ! ,
httpMethod : 'GET' ,
pathname : '/' ,
path : '/?query=test' ,
resHeaders : result.resHeaders ,
reqHeaders : result.reqHeaders ,
component : 'http' ,
} ;
assert . strictEqual ( spans . length , 2 ) ;
assert . strictEqual ( span . name , 'GET' ) ;
assert . strictEqual ( result . reqHeaders [ 'x-foo' ] , 'foo' ) ;
assert . strictEqual (
span . attributes [ SEMATTRS_HTTP_FLAVOR ] ,
HTTPFLAVORVALUES_HTTP_1_1
) ;
assert . strictEqual (
span . attributes [ SEMATTRS_NET_TRANSPORT ] ,
NETTRANSPORTVALUES_IP_TCP
) ;
assertSpan ( span , SpanKind . CLIENT , validations ) ;
} ) ;
it ( 'custom attributes should show up on client spans' , async ( ) = > {
const result = await httpRequest . get (
` ${ protocol } ://localhost: ${ mockServerPort } / `
) ;
const spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( s = > s . kind === SpanKind . CLIENT ) ;
assert . ok ( span ) ;
const validations = {
hostname : 'localhost' ,
httpStatusCode : result.statusCode ! ,
httpMethod : 'GET' ,
pathname : '/' ,
resHeaders : result.resHeaders ,
reqHeaders : result.reqHeaders ,
component : 'http' ,
} ;
assert . strictEqual ( spans . length , 2 ) ;
assert . strictEqual ( span . name , 'GET' ) ;
assert . strictEqual ( span . attributes [ 'span kind' ] , SpanKind . CLIENT ) ;
assertSpan ( span , SpanKind . CLIENT , validations ) ;
} ) ;
it ( 'should not mutate given headers object when adding propagation headers' , async ( ) = > {
const spans = memoryExporter . getFinishedSpans ( ) ;
assert . strictEqual ( spans . length , 0 ) ;
const headers = { 'x-foo' : 'foo' } ;
const result = await httpRequest . get (
new url . URL ( ` ${ protocol } ://localhost: ${ mockServerPort } /?query=test ` ) ,
{ headers }
) ;
assert . deepStrictEqual ( headers , { 'x-foo' : 'foo' } ) ;
assert . ok ( result . reqHeaders [ DummyPropagation . TRACE_CONTEXT_KEY ] ) ;
assert . ok ( result . reqHeaders [ DummyPropagation . SPAN_CONTEXT_KEY ] ) ;
} ) ;
it ( 'should create a span for GET requests and add propagation headers with Expect headers' , async ( ) = > {
let spans = memoryExporter . getFinishedSpans ( ) ;
assert . strictEqual ( spans . length , 0 ) ;
const options = Object . assign (
{ headers : { Expect : '100-continue' } } ,
url . parse ( ` ${ protocol } ://localhost: ${ mockServerPort } / ` )
) ;
const result = await httpRequest . get ( options ) ;
spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( s = > s . kind === SpanKind . CLIENT ) ;
assert . ok ( span ) ;
const validations = {
hostname : 'localhost' ,
httpStatusCode : 200 ,
httpMethod : 'GET' ,
pathname : '/' ,
resHeaders : result.resHeaders ,
reqHeaders : result.reqHeaders ,
component : 'http' ,
} ;
assert . strictEqual ( spans . length , 2 ) ;
assert . strictEqual ( span . name , 'GET' ) ;
assertSpan ( span , SpanKind . CLIENT , validations ) ;
} ) ;
for ( const headers of [
{ Expect : '100-continue' , 'user-agent' : 'http-plugin-test' } ,
{ 'user-agent' : 'http-plugin-test' } ,
] ) {
it ( ` should create a span for GET requests and add propagation when using the following signature: get(url, options, callback) and following headers: ${ JSON . stringify (
headers
) } ` , done => {
let validations : {
hostname : string ;
httpStatusCode : number ;
httpMethod : string ;
pathname : string ;
reqHeaders : http.OutgoingHttpHeaders ;
resHeaders : http.IncomingHttpHeaders ;
} ;
let data = '' ;
const spans = memoryExporter . getFinishedSpans ( ) ;
assert . strictEqual ( spans . length , 0 ) ;
const options = { headers } ;
const req = http . get (
` ${ protocol } ://localhost: ${ mockServerPort } / ` ,
options ,
( resp : http.IncomingMessage ) = > {
const res = resp as unknown as http . IncomingMessage & {
req : http.IncomingMessage ;
} ;
resp . on ( 'data' , chunk = > {
data += chunk ;
} ) ;
resp . on ( 'end' , ( ) = > {
validations = {
hostname : 'localhost' ,
httpStatusCode : 301 ,
httpMethod : 'GET' ,
pathname : '/' ,
resHeaders : resp.headers ,
/* tslint:disable:no-any */
reqHeaders : ( res . req as any ) . getHeaders
? ( res . req as any ) . getHeaders ( )
: ( res . req as any ) . _headers ,
/* tslint:enable:no-any */
} ;
} ) ;
}
) ;
req . on ( 'close' , ( ) = > {
const spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( s = > s . kind === SpanKind . CLIENT ) ;
assert . ok ( span ) ;
assert . strictEqual ( spans . length , 2 ) ;
assert . strictEqual ( span . name , 'GET' ) ;
assert . ok ( data ) ;
assert . ok ( validations . reqHeaders [ DummyPropagation . TRACE_CONTEXT_KEY ] ) ;
assert . ok ( validations . reqHeaders [ DummyPropagation . SPAN_CONTEXT_KEY ] ) ;
done ( ) ;
} ) ;
} ) ;
}
it ( 'should work for multiple active requests in keep-alive mode' , async ( ) = > {
await sendRequestTwice ( hostname , mockServerPort ) ;
const spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( ( s : any ) = > s . kind === SpanKind . SERVER ) ;
assert . ok ( span ) ;
assert . strictEqual ( spans . length , 2 ) ;
assert . strictEqual ( span . name , 'GET' ) ;
} ) ;
it ( 'should have correct spans even when request timeout' , async ( ) = > {
let spans = memoryExporter . getFinishedSpans ( ) ;
assert . strictEqual ( spans . length , 0 ) ;
try {
await httpRequest . get (
` ${ protocol } ://localhost: ${ mockServerPort } /timeout ` ,
{ timeout : 1 }
) ;
2024-08-08 12:04:12 +08:00
} catch ( err : any ) {
2024-08-08 10:58:05 +08:00
assert . ok ( err . message . startsWith ( 'timeout' ) ) ;
}
spans = memoryExporter . getFinishedSpans ( ) ;
const span = spans . find ( s = > s . kind === SpanKind . CLIENT ) ;
assert . ok ( span ) ;
assert . strictEqual ( span . name , 'GET' ) ;
assert . strictEqual (
span . attributes [ SEMATTRS_HTTP_HOST ] ,
` localhost: ${ mockServerPort } `
) ;
} ) ;
} ) ;
} ) ;