From 4b140a3c6b8d427f8467a3a409963fddb988153b Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Sun, 4 Aug 2019 19:30:51 -0400 Subject: [PATCH 01/18] feat(plugin): add http plugin Closes #157 Signed-off-by: Olivier Albertini --- .../opentelemetry-node-tracer/src/index.ts | 2 +- .../test/NodeTracer.test.ts | 1 + .../opentelemetry-plugin-http/package.json | 13 +- .../opentelemetry-plugin-http/src/http.ts | 450 ++++++++++++++++++ .../opentelemetry-plugin-http/src/types.ts | 30 ++ .../test/http-disable.test.ts | 120 +++++ .../test/http-enable.test.ts | 95 ++++ .../test/http-static-methods.test.ts | 135 ++++++ 8 files changed, 844 insertions(+), 2 deletions(-) create mode 100644 packages/opentelemetry-plugin-http/src/types.ts create mode 100644 packages/opentelemetry-plugin-http/test/http-disable.test.ts create mode 100644 packages/opentelemetry-plugin-http/test/http-enable.test.ts create mode 100644 packages/opentelemetry-plugin-http/test/http-static-methods.test.ts diff --git a/packages/opentelemetry-node-tracer/src/index.ts b/packages/opentelemetry-node-tracer/src/index.ts index 0478f974a4..310d6a16a9 100644 --- a/packages/opentelemetry-node-tracer/src/index.ts +++ b/packages/opentelemetry-node-tracer/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './NodeTracer'; + export * from './NodeTracer'; diff --git a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts index c84fa6a5ab..2882385008 100644 --- a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts +++ b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts @@ -25,6 +25,7 @@ import { } from '@opentelemetry/core'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { NodeTracer } from '../src/NodeTracer'; +import { EventEmitter } from 'events'; describe('NodeTracer', () => { describe('constructor', () => { diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index 9a7819670c..bb4800af74 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -38,12 +38,19 @@ "access": "public" }, "devDependencies": { + "@opentelemetry/scope-async-hooks": "^0.0.1", "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", + "@types/semver": "^6.0.1", + "@types/shimmer": "^1.0.1", + "@types/sinon":"^7.0.13", "codecov": "^3.5.0", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", + "sinon": "^7.3.2", + "c8": "^5.0.1", + "nock": "^10.0.6", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", "typescript": "^3.5.3" @@ -51,6 +58,10 @@ "dependencies": { "@opentelemetry/core": "^0.0.1", "@opentelemetry/node-tracer": "^0.0.1", - "@opentelemetry/types": "^0.0.1" + "@opentelemetry/types": "^0.0.1", + "@types/nock": "^10.0.3", + "nock": "^10.0.6", + "semver": "^6.3.0", + "shimmer": "^1.2.1" } } diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index e69de29bb2..512f8517bf 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -0,0 +1,450 @@ +/** + * Copyright 2019, 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 { BasePlugin, NoopLogger, isValid } from '@opentelemetry/core'; +import { CanonicalCode, Span, SpanKind, SpanOptions, Logger, Status } from '@opentelemetry/types'; +import { NodeTracer } from '@opentelemetry/node-tracer'; +import { ClientRequest, IncomingMessage, request, get, RequestOptions, ServerResponse } from 'http'; +import * as http from 'http'; +import * as semver from 'semver'; +import * as shimmer from 'shimmer'; +import * as url from 'url'; +import { HttpPluginConfig, IgnoreMatcher } from './types'; + +export type HttpCallback = (res: IncomingMessage) => void; +export type RequestFunction = typeof request; +export type GetFunction = typeof get; +export type Http = typeof http; + +/** + * Default type for functions + * @TODO: export this to types package + */ +type Func = (...args: any[]) => T; + +/** + * Http instrumentation plugin for Opentelemetry + */ +export class HttpPlugin extends BasePlugin { + /** + * Attributes Names according to Opencensus HTTP Specs since there is no specific OpenTelemetry Attributes + * https://github.com/open-telemetry/opentelemetry-specification/blob/master/work_in_progress/opencensus/HTTP.md#attributes + */ + static ATTRIBUTE_HTTP_HOST = 'http.host'; + // NOT ON OFFICIAL SPEC + static ATTRIBUTE_ERROR = 'error'; + 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'; + // NOT ON OFFICIAL SPEC + static ATTRIBUTE_HTTP_ERROR_NAME = 'http.error_name'; + static ATTRIBUTE_HTTP_ERROR_MESSAGE = 'http.error_message'; + static PROPAGATION_FORMAT = 'HttpTraceContext'; + + /** + * Parse status code from HTTP response. + */ + static parseResponseStatus(statusCode: number): Omit { + if (statusCode < 200 || statusCode > 504) { + return { code: CanonicalCode.UNKNOWN }; + } else if (statusCode >= 200 && statusCode < 400) { + return { code: CanonicalCode.OK }; + } else { + switch (statusCode) { + case 400: + return { code: CanonicalCode.INVALID_ARGUMENT }; + case 504: + return { code: CanonicalCode.DEADLINE_EXCEEDED }; + case 404: + return { code: CanonicalCode.NOT_FOUND }; + case 403: + return { code: CanonicalCode.PERMISSION_DENIED }; + case 401: + return { code: CanonicalCode.UNAUTHENTICATED }; + case 429: + return { code: CanonicalCode.RESOURCE_EXHAUSTED }; + case 501: + return { code: CanonicalCode.UNIMPLEMENTED }; + case 503: + return { code: CanonicalCode.UNAVAILABLE }; + default: + return { code: CanonicalCode.UNKNOWN }; + } + } + } + + /** + * Returns whether the Expect header is on the given options object. + * @param options Options for http.request. + */ + static hasExpectHeader(options: RequestOptions | url.URL): boolean { + return !!((options as RequestOptions).headers && (options as RequestOptions).headers!.Expect); + } + + /** + * Check whether the given request match pattern + * @param url URL of request + * @param request Request to inspect + * @param pattern Match pattern + */ + static isSatisfyPattern(url: string, request: T, pattern: IgnoreMatcher): boolean { + 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'); + } + } + + /** + * Check whether the given request is ignored by configuration + * @param url URL of request + * @param request Request to inspect + * @param list List of ignore patterns + */ + static isIgnored(url: string, request: T, list?: Array>): boolean { + if (!list) { + // No ignored urls - trace everything + return false; + } + + for (const pattern of list) { + if (HttpPlugin.isSatisfyPattern(url, request, pattern)) { + return true; + } + } + + return false; + } + + static setSpanOnError(span: Span, obj: NodeJS.EventEmitter) { + obj.on('error', error => { + span + .setAttribute(HttpPlugin.ATTRIBUTE_ERROR, true) + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_NAME, error.name) + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_MESSAGE, error.message); + span.setStatus({ code: CanonicalCode.UNKNOWN, message: error.message }); + span.end(); + }); + } + + options!: HttpPluginConfig; + + // TODO: BasePlugin should pass the logger or when we enable the plugin + protected readonly _logger!: Logger; + protected readonly _tracer!: NodeTracer; + + /** Constructs a new HttpPlugin instance. */ + constructor(public moduleName: string, public version: string) { + super(); + // TODO: remove this once a logger will be passed + this._logger = new NoopLogger(); + } + + /** Patches HTTP incoming and outcoming request functions. */ + protected patch() { + this._logger.debug('applying patch to %s@%s', this.moduleName, this.version); + + shimmer.wrap(this._moduleExports, 'request', this.getPatchOutgoingRequestFunction()); + + // In Node 8-10, http.get calls a private request method, therefore we patch it + // here too. + if (semver.satisfies(this.version, '>=8.0.0')) { + shimmer.wrap(this._moduleExports, 'get', this.getPatchOutgoingGetFunction()); + } + + 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 unpatch(): void { + 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'); + } + } + + /** + * Creates spans for incoming requests, restoring spans' context if applied. + */ + protected getPatchIncomingRequestFunction() { + return (original: (event: string) => boolean) => { + return this.incomingRequestFunction(original, this); + }; + } + + /** + * Creates spans for outgoing requests, sending spans' context for distributed + * tracing. + */ + protected getPatchOutgoingRequestFunction() { + return (original: Func): Func => { + return this.outgoingRequestFunction(original, this); + }; + } + + protected getPatchOutgoingGetFunction() { + return (original: Func): Func => { + // Re-implement http.get. This needs to be done (instead of using + // getPatchOutgoingRequestFunction to patch it) because we need to + // set the trace context header before the returned ClientRequest is + // ended. The Node.js docs state that the only differences between + // request and get are that (1) get defaults to the HTTP GET method and + // (2) the returned request object is ended immediately. The former is + // already true (at least in supported Node versions up to v10), so we + // simply follow the latter. Ref: + // https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback + // https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/plugins/plugin-http.ts#L198 + return function outgoingGetRequest(options: string | RequestOptions | URL, callback?: HttpCallback) { + const req = request(options, callback); + req.end(); + return req; + }; + }; + } + + /** + * Injects span's context to header for distributed tracing and finshes the + * span when the response is finished. + * @param original The original patched function. + * @param options The arguments to the original function. + */ + private getMakeRequestTraceFunction( + request: ClientRequest, + options: RequestOptions, + plugin: HttpPlugin + ): Func { + return (span: Span): ClientRequest => { + plugin._logger.debug('makeRequestTrace'); + + if (!span) { + plugin._logger.debug('makeRequestTrace span is null'); + return request; + } + + const propagation = plugin._tracer.getHttpTextFormat(); + // If outgoing request headers contain the "Expect" header, the returned + // ClientRequest will throw an error if any new headers are added. + // So we need to clone the options object to be able to inject new + // header. + if (HttpPlugin.hasExpectHeader(options)) { + options = Object.assign({}, options); + options.headers = Object.assign({}, options.headers); + } + propagation.inject(span.context(), HttpPlugin.PROPAGATION_FORMAT, options.headers); + + request.on('response', (response: IncomingMessage) => { + 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.toUpperCase() : 'GET'; + const headers = options.headers; + const userAgent = headers ? headers['user-agent'] || headers['User-Agent'] : null; + + const host = options.hostname || options.host || 'localhost'; + span + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host) + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, method); + if (options.path) { + span + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, options.path) + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, options.path); + } + + if (userAgent) { + span.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent.toString()); + } + if (response.statusCode) { + span.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()); + span.setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); + } + + if (plugin.options.applyCustomAttributesOnSpan) { + plugin.options.applyCustomAttributesOnSpan(span, request, response); + } + + span.end(); + }); + HttpPlugin.setSpanOnError(span, response); + }); + + HttpPlugin.setSpanOnError(span, request); + + plugin._logger.debug('makeRequestTrace return request'); + return request; + }; + } + + private incomingRequestFunction(original: (event: string) => boolean, plugin: HttpPlugin) { + return function incomingRequest(event: string, ...args: unknown[]): boolean { + // Only traces request events + if (event !== 'request') { + // @ts-ignore @TODO: remove ts-ignore and find how to type this + return original.apply(this, arguments); + } + + const request = args[0] as IncomingMessage; + const response = args[1] as ServerResponse; + const path = request.url ? url.parse(request.url).pathname || '' : ''; + const method = request.method || 'GET'; + plugin._logger.debug('%s plugin incomingRequest', plugin.moduleName); + + if (HttpPlugin.isIgnored(path, request, plugin.options.ignoreIncomingPaths)) { + // @ts-ignore @TODO: remove ts-ignore and find how to type this + return original.apply(this, arguments); + } + + const propagation = plugin._tracer.getHttpTextFormat(); + const headers = request.headers; + + const spanOptions: SpanOptions = { + kind: SpanKind.SERVER + }; + + const spanContext = propagation.extract(HttpPlugin.PROPAGATION_FORMAT, headers); + if (spanContext && isValid(spanContext)) { + spanOptions.parent = spanContext; + } + + const rootSpan = plugin._tracer.startSpan(path, spanOptions); + return plugin._tracer.withSpan(rootSpan, () => { + plugin._tracer.wrapEmitter(request); + plugin._tracer.wrapEmitter(response); + + // Wraps end (inspired by: + // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75) + const originalEnd = response.end; + + response.end = function(this: ServerResponse) { + response.end = originalEnd; + // @ts-ignore @TODO: remove ts-ignore and find how to type this + const returned = response.end.apply(this, arguments); + const requestUrl = request.url ? url.parse(request.url) : null; + const host = headers.host || 'localhost'; + const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; + + rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')); + + rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, method); + if (requestUrl) { + rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, requestUrl.pathname || ''); + rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, requestUrl.path || ''); + } + if (userAgent) { + rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent); + } + rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()); + + rootSpan.setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); + + if (plugin.options.applyCustomAttributesOnSpan) { + plugin.options.applyCustomAttributesOnSpan(rootSpan, request, response); + } + + rootSpan.end(); + return returned; + }; + // @ts-ignore @TODO: remove ts-ignore and find how to type this, arguments + return original.apply(this, arguments); + }); + }; + } + + private outgoingRequestFunction(original: Func, plugin: HttpPlugin): Func { + return function outgoingRequest(options: RequestOptions | string, callback?: HttpCallback): ClientRequest { + if (!options) { + // @ts-ignore @TODO: remove ts-ignore and find how to type this + return original.apply(this, [options, callback]); + } + + // Makes sure the url is an url object + let pathname; + let method; + let origin = ''; + if (typeof options === 'string') { + const parsedUrl = url.parse(options); + options = parsedUrl; + pathname = parsedUrl.pathname || ''; + origin = `${parsedUrl.protocol || 'http:'}//${parsedUrl.host}`; + } else { + try { + pathname = (options as url.URL).pathname; + if (!pathname) { + pathname = options.path ? url.parse(options.path).pathname : ''; + } + method = options.method; + origin = `${options.protocol || 'http:'}//${options.host}`; + } catch (ignore) {} + } + // @ts-ignore @TODO: remove ts-ignore and find how to type this + const request: ClientRequest = original.apply(this, [options, callback]); + + if (HttpPlugin.isIgnored(origin + pathname, request, plugin.options.ignoreOutgoingUrls)) { + return request; + } + + plugin._tracer.wrapEmitter(request); + + if (!method) { + method = 'GET'; + } else { + // some packages return method in lowercase.. + // ensure upperCase for consistency + method = method.toUpperCase(); + } + + plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); + const operationName = `${method} ${pathname}`; + const spanOptions = { + kind: SpanKind.CLIENT + }; + const currentSpan = plugin._tracer.getCurrentSpan(); + // Checks if this outgoing request is part of an operation by checking + // if there is a current root span, if so, we create a child span. In + // case there is no root span, this means that the outgoing request is + // the first operation, therefore we create a root span. + if (!currentSpan) { + plugin._logger.debug('outgoingRequest starting a root span'); + const rootSpan = plugin._tracer.startSpan(operationName, spanOptions); + return plugin._tracer.withSpan(rootSpan, plugin.getMakeRequestTraceFunction(request, options, plugin)); + } else { + plugin._logger.debug('outgoingRequest starting a child span'); + const span = plugin._tracer.startSpan(operationName, { + kind: spanOptions.kind, + parent: currentSpan + }); + return plugin.getMakeRequestTraceFunction(request, options, plugin)(span); + } + }; + } +} + +export const plugin = new HttpPlugin('http', '8.0.0'); diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts new file mode 100644 index 0000000000..a4d5d6f735 --- /dev/null +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2019, 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 { Span } from '@opentelemetry/types'; +import { ClientRequest, IncomingMessage, ServerResponse } from 'http'; + +export type IgnoreMatcher = string | RegExp | ((url: string, request: T) => boolean); + +export interface HttpCustomAttributeFunction { + (span: Span, request: ClientRequest | IncomingMessage, response: IncomingMessage | ServerResponse): void; +} + +export interface HttpPluginConfig { + ignoreIncomingPaths?: Array>; + ignoreOutgoingUrls?: Array>; + applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; +} diff --git a/packages/opentelemetry-plugin-http/test/http-disable.test.ts b/packages/opentelemetry-plugin-http/test/http-disable.test.ts new file mode 100644 index 0000000000..f590914af0 --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/http-disable.test.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2019, 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 * as assert from 'assert'; +import * as http from 'http'; +import * as nock from 'nock'; +import * as sinon from 'sinon'; + +import { plugin } from '../src/http'; +import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; +import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; +import { NodeTracer } from '@opentelemetry/node-tracer'; +import { NoopLogger } from '@opentelemetry/core'; +import { AddressInfo } from 'net'; + +const httpRequest = { + get: (options: {} | string) => { + return new Promise((resolve, reject) => { + return http.get(options, resp => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + resp.on('error', err => { + reject(err); + }); + }); + }); + }, +}; + +class DummyPropagation implements HttpTextFormat { + extract(headers: any): SpanContext { + return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; + } + + inject(spanContext: SpanContext, format: string, headers: any): void { + headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; + headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; + } +} + +describe('HttpPlugin', () => { + + let server: http.Server; + let serverPort = 0; + + describe('disable()', () => { + const scopeManager = new AsyncHooksScopeManager(); + const httpTextFormat = new DummyPropagation(); + const logger = new NoopLogger(); + const tracer = new NodeTracer({ + scopeManager, + logger, + httpTextFormat, + }); + before(() => { + plugin.enable( + http, + tracer + ); + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + + server.listen(serverPort); + server.once('listening', () => { + serverPort = (server.address() as AddressInfo).port; + }); + }); + + beforeEach(() => { + nock.cleanAll(); + tracer.startSpan = sinon.spy(); + tracer.withSpan = sinon.spy(); + tracer.wrapEmitter = sinon.spy(); + tracer.recordSpanData = sinon.spy(); + }); + + afterEach(() => { + nock.cleanAll(); + sinon.restore(); + }); + + after(() => { + server.close(); + }); + describe('unpatch()', () => { + it('should not call tracer methods for creating span', async () => { + plugin.disable(); + const testPath = '/incoming/unpatch/'; + + const options = { host: 'localhost', path: testPath, port: serverPort }; + + await httpRequest.get(options).then(result => { + assert.strictEqual((tracer.startSpan as sinon.SinonSpy).called, false); + assert.strictEqual((tracer.withSpan as sinon.SinonSpy).called, false); + assert.strictEqual((tracer.wrapEmitter as sinon.SinonSpy).called, false); + assert.strictEqual((tracer.recordSpanData as sinon.SinonSpy).called, false); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/http-enable.test.ts new file mode 100644 index 0000000000..ea611e9c1f --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/http-enable.test.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2019, 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 * as assert from 'assert'; +import * as http from 'http'; +import * as nock from 'nock'; + +import { HttpPlugin, plugin } from '../src/http'; +import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; +import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; +import { NodeTracer } from '@opentelemetry/node-tracer'; +import { NoopLogger } from '@opentelemetry/core'; +import { AddressInfo } from 'net'; + +class DummyPropagation implements HttpTextFormat { + extract(headers: any): SpanContext { + return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; + } + + inject(spanContext: SpanContext, format: string, headers: any): void { + headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; + headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; + } +} + +describe('HttpPlugin', () => { + + let server: http.Server; + let serverPort = 0; + + describe('enable()', () => { + + const scopeManager = new AsyncHooksScopeManager(); + const httpTextFormat = new DummyPropagation(); + const logger = new NoopLogger(); + const tracer = new NodeTracer({ + scopeManager, + logger, + httpTextFormat, + }); + before(() => { + plugin.enable( + http, + tracer, + // { + // ignoreIncomingPaths: [ + // '/ignored/string', + // /^\/ignored\/regexp/, + // (url: string) => url === '/ignored/function', + // ], + // ignoreOutgoingUrls: [ + // `${urlHost}/ignored/string`, + // /^http:\/\/fake\.service\.io\/ignored\/regexp$/, + // (url: string) => url === `${urlHost}/ignored/function`, + // ], + // applyCustomAttributesOnSpan: customAttributeFunction, + // }, + ); + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + + server.listen(serverPort); + server.once('listening', () => { + serverPort = (server.address() as AddressInfo).port; + }); + nock.disableNetConnect(); + }); + + beforeEach(() => { + nock.cleanAll(); + }); + + after(() => { + server.close(); + }); + + it('should return a plugin', () => { + assert.ok(plugin instanceof HttpPlugin); + }); + }); +}); diff --git a/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts b/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts new file mode 100644 index 0000000000..8e3be034fb --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts @@ -0,0 +1,135 @@ +/** + * Copyright 2019, 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 * as assert from 'assert'; +import * as sinon from 'sinon'; +import { CanonicalCode } from '@opentelemetry/types'; +import { HttpPlugin } from '../src/http'; + +describe.only('Static HttpPlugin methods', () => { + describe('parseResponseStatus()', () => { + + it('should return UNKNOWN code by default', () => { + const status = HttpPlugin.parseResponseStatus(undefined as any); + assert.deepStrictEqual(status, { code: CanonicalCode.UNKNOWN }); + }); + + it('should return OK for Success HTTP status code', () => { + for (let index = 200; index < 400; index++) { + const status = HttpPlugin.parseResponseStatus(index); + assert.deepStrictEqual(status, { code: CanonicalCode.OK }); + } + }); + + it('should not return OK for Bad HTTP status code', () => { + for (let index = 400; index <= 504; index++) { + const status = HttpPlugin.parseResponseStatus(index); + assert.notStrictEqual(status.code, CanonicalCode.OK); + } + }); + }); + describe('hasExpectHeader()', () => { + it('should throw if no option', () => { + try { + HttpPlugin.hasExpectHeader(undefined as any); + assert.fail() + } catch (ignore) {} + }); + + it('should not throw if no headers', () => { + const result = HttpPlugin.hasExpectHeader({} as any); + assert.strictEqual(result, false); + }); + + it('should return true on Expect', () => { + const result = HttpPlugin.hasExpectHeader({ headers: { Expect: {} } } as any); + assert.strictEqual(result, true); + }); + + }); + describe('isSatisfyPattern()', () => { + + it('string pattern', () => { + const answer1 = HttpPlugin.isSatisfyPattern('/test/1', {}, '/test/1'); + assert.strictEqual(answer1, true); + const answer2 = HttpPlugin.isSatisfyPattern('/test/1', {}, '/test/11'); + assert.strictEqual(answer2, false); + }); + + it('regex pattern', () => { + const answer1 = HttpPlugin.isSatisfyPattern('/TeSt/1', {}, /\/test/i); + assert.strictEqual(answer1, true); + const answer2 = HttpPlugin.isSatisfyPattern('/2/tEst/1', {}, /\/test/); + assert.strictEqual(answer2, false); + }); + + it('should throw if type is unknown', () => { + + try { + HttpPlugin.isSatisfyPattern('/TeSt/1', {}, true as any); + assert.fail() + } catch (error) { + assert.strictEqual(error instanceof TypeError, true); + } + }); + + + it('function pattern', () => { + const answer1 = HttpPlugin.isSatisfyPattern('/test/home', { headers: {}}, (url: string, req: { headers: any }) => req.headers && url === '/test/home'); + assert.strictEqual(answer1, true); + const answer2 = HttpPlugin.isSatisfyPattern('/test/home', { headers: {}}, (url: string, req: { headers: any }) => url !== '/test/home'); + assert.strictEqual(answer2, false); + }); + }); + + describe('isIgnored()', () => { + + beforeEach(() => { + HttpPlugin.isSatisfyPattern = sinon.spy(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should call isSatisfyPattern, n match', () => { + const answer1 = HttpPlugin.isIgnored('/test/1', {}, ['/test/11']); + assert.strictEqual(answer1, false); + assert.strictEqual((HttpPlugin.isSatisfyPattern as sinon.SinonSpy).callCount, 1); + }); + + it('should call isSatisfyPattern, match', () => { + const answer1 = HttpPlugin.isIgnored('/test/1', {}, ['/test/11']); + assert.strictEqual(answer1, false); + assert.strictEqual((HttpPlugin.isSatisfyPattern as sinon.SinonSpy).callCount, 1); + }); + + it('should not call isSatisfyPattern', () => { + HttpPlugin.isIgnored('/test/1', {}, []); + assert.strictEqual((HttpPlugin.isSatisfyPattern as sinon.SinonSpy).callCount, 0); + }); + + it('should return false on empty list', () => { + const answer1 = HttpPlugin.isIgnored('/test/1', {}, []); + assert.strictEqual(answer1, false); + }); + + it('should not throw and return false when list is undefined', () => { + const answer2 = HttpPlugin.isIgnored('/test/1', {}, undefined); + assert.strictEqual(answer2, false); + }); + }); +}); From 5922845b7f1ff861b74a25a30b4ef6151f3b77a2 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Mon, 5 Aug 2019 11:47:29 -0400 Subject: [PATCH 02/18] fix: integrate vmarchaud recommandations Signed-off-by: Olivier Albertini --- .../test/NodeTracer.test.ts | 1 - packages/opentelemetry-plugin-http/.DS_Store | Bin 0 -> 6148 bytes .../opentelemetry-plugin-http/package.json | 3 +- .../opentelemetry-plugin-http/src/http.ts | 53 +++++++----------- .../opentelemetry-plugin-http/src/types.ts | 13 ++++- .../test/http-disable.test.ts | 16 ++---- .../test/http-enable.test.ts | 10 ++-- .../test/http-static-methods.test.ts | 36 ++++++------ 8 files changed, 62 insertions(+), 70 deletions(-) create mode 100644 packages/opentelemetry-plugin-http/.DS_Store diff --git a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts index 2882385008..c84fa6a5ab 100644 --- a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts +++ b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts @@ -25,7 +25,6 @@ import { } from '@opentelemetry/core'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { NodeTracer } from '../src/NodeTracer'; -import { EventEmitter } from 'events'; describe('NodeTracer', () => { describe('constructor', () => { diff --git a/packages/opentelemetry-plugin-http/.DS_Store b/packages/opentelemetry-plugin-http/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a2eb533abf551d042629249e6fe5fb378b3b5ee0 GIT binary patch literal 6148 zcmeH~u?oUK42Bc!Ah>jNyu}Cb4Gz&K=nFU~E>c0O^F6wMazU^xC+1b{XuyJ79K1T}(S!u1*@b}wNMJ-@TJzTK|1JE}{6A`8N&+PC zX9Tp_belC^D(=>|*R%RAs void; -export type RequestFunction = typeof request; -export type GetFunction = typeof get; -export type Http = typeof http; - -/** - * Default type for functions - * @TODO: export this to types package - */ -type Func = (...args: any[]) => T; +import { HttpPluginConfig, IgnoreMatcher, Http, Func, HttpCallback, ResponseEndArgs } from './types'; /** * Http instrumentation plugin for Opentelemetry @@ -261,7 +249,7 @@ export class HttpPlugin extends BasePlugin { propagation.inject(span.context(), HttpPlugin.PROPAGATION_FORMAT, options.headers); request.on('response', (response: IncomingMessage) => { - plugin._tracer.wrapEmitter(response); + plugin._tracer.scopeManager.bind(response); plugin._logger.debug('outgoingRequest on response()'); response.on('end', () => { plugin._logger.debug('outgoingRequest on end()'); @@ -303,12 +291,11 @@ export class HttpPlugin extends BasePlugin { }; } - private incomingRequestFunction(original: (event: string) => boolean, plugin: HttpPlugin) { - return function incomingRequest(event: string, ...args: unknown[]): boolean { + private incomingRequestFunction(original: (event: string, ...args: unknown[]) => boolean, plugin: HttpPlugin) { + return function incomingRequest(this: {}, event: string, ...args: unknown[]): boolean { // Only traces request events if (event !== 'request') { - // @ts-ignore @TODO: remove ts-ignore and find how to type this - return original.apply(this, arguments); + return original.apply(this, [event, ...args]); } const request = args[0] as IncomingMessage; @@ -318,8 +305,7 @@ export class HttpPlugin extends BasePlugin { plugin._logger.debug('%s plugin incomingRequest', plugin.moduleName); if (HttpPlugin.isIgnored(path, request, plugin.options.ignoreIncomingPaths)) { - // @ts-ignore @TODO: remove ts-ignore and find how to type this - return original.apply(this, arguments); + return original.apply(this, [event, ...args]); } const propagation = plugin._tracer.getHttpTextFormat(); @@ -336,17 +322,17 @@ export class HttpPlugin extends BasePlugin { const rootSpan = plugin._tracer.startSpan(path, spanOptions); return plugin._tracer.withSpan(rootSpan, () => { - plugin._tracer.wrapEmitter(request); - plugin._tracer.wrapEmitter(response); + plugin._tracer.scopeManager.bind(request); + plugin._tracer.scopeManager.bind(response); // Wraps end (inspired by: // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75) const originalEnd = response.end; - - response.end = function(this: ServerResponse) { + response.end = function(this: ServerResponse, ...args: ResponseEndArgs) { response.end = originalEnd; - // @ts-ignore @TODO: remove ts-ignore and find how to type this - const returned = response.end.apply(this, arguments); + // Cannot pass args of type ResponseEndArgs, Expected 1-2 arguments, but got 1 or more. + // tslint:disable-next-line:no-any + const returned = response.end.apply(this, arguments as any); const requestUrl = request.url ? url.parse(request.url) : null; const host = headers.host || 'localhost'; const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; @@ -372,16 +358,18 @@ export class HttpPlugin extends BasePlugin { rootSpan.end(); return returned; }; - // @ts-ignore @TODO: remove ts-ignore and find how to type this, arguments - return original.apply(this, arguments); + return original.apply(this, [event, ...args]); }); }; } private outgoingRequestFunction(original: Func, plugin: HttpPlugin): Func { - return function outgoingRequest(options: RequestOptions | string, callback?: HttpCallback): ClientRequest { + return function outgoingRequest( + this: {}, + options: RequestOptions | string, + callback?: HttpCallback + ): ClientRequest { if (!options) { - // @ts-ignore @TODO: remove ts-ignore and find how to type this return original.apply(this, [options, callback]); } @@ -404,14 +392,13 @@ export class HttpPlugin extends BasePlugin { origin = `${options.protocol || 'http:'}//${options.host}`; } catch (ignore) {} } - // @ts-ignore @TODO: remove ts-ignore and find how to type this const request: ClientRequest = original.apply(this, [options, callback]); if (HttpPlugin.isIgnored(origin + pathname, request, plugin.options.ignoreOutgoingUrls)) { return request; } - plugin._tracer.wrapEmitter(request); + plugin._tracer.scopeManager.bind(request); if (!method) { method = 'GET'; diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index a4d5d6f735..2db0be2c52 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -15,9 +15,20 @@ */ import { Span } from '@opentelemetry/types'; -import { ClientRequest, IncomingMessage, ServerResponse } from 'http'; +import { ClientRequest, IncomingMessage, ServerResponse, request, get } from 'http'; +import * as http from 'http'; export type IgnoreMatcher = string | RegExp | ((url: string, request: T) => boolean); +export type HttpCallback = (res: IncomingMessage) => void; +export type RequestFunction = typeof request; +export type GetFunction = typeof get; +export type Http = typeof http; +/* tslint:disable-next-line:no-any */ +export type Func = (...args: any[]) => T; +export type ResponseEndArgs = + | [((() => void) | undefined)?] + | [unknown, ((() => void) | undefined)?] + | [unknown, string, ((() => void) | undefined)?]; export interface HttpCustomAttributeFunction { (span: Span, request: ClientRequest | IncomingMessage, response: IncomingMessage | ServerResponse): void; diff --git a/packages/opentelemetry-plugin-http/test/http-disable.test.ts b/packages/opentelemetry-plugin-http/test/http-disable.test.ts index f590914af0..a3dd741fe6 100644 --- a/packages/opentelemetry-plugin-http/test/http-disable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-disable.test.ts @@ -42,22 +42,21 @@ const httpRequest = { }); }); }); - }, + } }; class DummyPropagation implements HttpTextFormat { - extract(headers: any): SpanContext { + extract(headers: unknown): SpanContext { return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; } - inject(spanContext: SpanContext, format: string, headers: any): void { + inject(spanContext: SpanContext, format: string, headers: http.IncomingHttpHeaders): void { headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; } } describe('HttpPlugin', () => { - let server: http.Server; let serverPort = 0; @@ -68,13 +67,10 @@ describe('HttpPlugin', () => { const tracer = new NodeTracer({ scopeManager, logger, - httpTextFormat, + httpTextFormat }); before(() => { - plugin.enable( - http, - tracer - ); + plugin.enable(http, tracer); server = http.createServer((request, response) => { response.end('Test Server Response'); }); @@ -89,7 +85,6 @@ describe('HttpPlugin', () => { nock.cleanAll(); tracer.startSpan = sinon.spy(); tracer.withSpan = sinon.spy(); - tracer.wrapEmitter = sinon.spy(); tracer.recordSpanData = sinon.spy(); }); @@ -111,7 +106,6 @@ describe('HttpPlugin', () => { await httpRequest.get(options).then(result => { assert.strictEqual((tracer.startSpan as sinon.SinonSpy).called, false); assert.strictEqual((tracer.withSpan as sinon.SinonSpy).called, false); - assert.strictEqual((tracer.wrapEmitter as sinon.SinonSpy).called, false); assert.strictEqual((tracer.recordSpanData as sinon.SinonSpy).called, false); }); }); diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/http-enable.test.ts index ea611e9c1f..d2fabc2c03 100644 --- a/packages/opentelemetry-plugin-http/test/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-enable.test.ts @@ -26,35 +26,33 @@ import { NoopLogger } from '@opentelemetry/core'; import { AddressInfo } from 'net'; class DummyPropagation implements HttpTextFormat { - extract(headers: any): SpanContext { + extract(format: string, carrier: unknown): SpanContext { return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; } - inject(spanContext: SpanContext, format: string, headers: any): void { + inject(spanContext: SpanContext, format: string, headers: http.IncomingHttpHeaders): void { headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; } } describe('HttpPlugin', () => { - let server: http.Server; let serverPort = 0; describe('enable()', () => { - const scopeManager = new AsyncHooksScopeManager(); const httpTextFormat = new DummyPropagation(); const logger = new NoopLogger(); const tracer = new NodeTracer({ scopeManager, logger, - httpTextFormat, + httpTextFormat }); before(() => { plugin.enable( http, - tracer, + tracer // { // ignoreIncomingPaths: [ // '/ignored/string', diff --git a/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts b/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts index 8e3be034fb..00f9c0d339 100644 --- a/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts @@ -18,12 +18,13 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { CanonicalCode } from '@opentelemetry/types'; import { HttpPlugin } from '../src/http'; +import { RequestOptions } from 'https'; +import { IgnoreMatcher } from '../src/types'; -describe.only('Static HttpPlugin methods', () => { +describe('Static HttpPlugin methods', () => { describe('parseResponseStatus()', () => { - it('should return UNKNOWN code by default', () => { - const status = HttpPlugin.parseResponseStatus(undefined as any); + const status = HttpPlugin.parseResponseStatus((undefined as unknown) as number); assert.deepStrictEqual(status, { code: CanonicalCode.UNKNOWN }); }); @@ -44,24 +45,22 @@ describe.only('Static HttpPlugin methods', () => { describe('hasExpectHeader()', () => { it('should throw if no option', () => { try { - HttpPlugin.hasExpectHeader(undefined as any); - assert.fail() + HttpPlugin.hasExpectHeader('' as RequestOptions); + assert.fail(); } catch (ignore) {} }); it('should not throw if no headers', () => { - const result = HttpPlugin.hasExpectHeader({} as any); + const result = HttpPlugin.hasExpectHeader({} as RequestOptions); assert.strictEqual(result, false); }); it('should return true on Expect', () => { - const result = HttpPlugin.hasExpectHeader({ headers: { Expect: {} } } as any); + const result = HttpPlugin.hasExpectHeader({ headers: { Expect: 1 } } as RequestOptions); assert.strictEqual(result, true); }); - }); describe('isSatisfyPattern()', () => { - it('string pattern', () => { const answer1 = HttpPlugin.isSatisfyPattern('/test/1', {}, '/test/1'); assert.strictEqual(answer1, true); @@ -77,26 +76,31 @@ describe.only('Static HttpPlugin methods', () => { }); it('should throw if type is unknown', () => { - try { - HttpPlugin.isSatisfyPattern('/TeSt/1', {}, true as any); - assert.fail() + HttpPlugin.isSatisfyPattern('/TeSt/1', {}, (true as unknown) as IgnoreMatcher<{}>); + assert.fail(); } catch (error) { assert.strictEqual(error instanceof TypeError, true); } }); - it('function pattern', () => { - const answer1 = HttpPlugin.isSatisfyPattern('/test/home', { headers: {}}, (url: string, req: { headers: any }) => req.headers && url === '/test/home'); + const answer1 = HttpPlugin.isSatisfyPattern( + '/test/home', + { headers: {} }, + (url: string, req: { headers: unknown }) => req.headers && url === '/test/home' + ); assert.strictEqual(answer1, true); - const answer2 = HttpPlugin.isSatisfyPattern('/test/home', { headers: {}}, (url: string, req: { headers: any }) => url !== '/test/home'); + const answer2 = HttpPlugin.isSatisfyPattern( + '/test/home', + { headers: {} }, + (url: string, req: { headers: unknown }) => url !== '/test/home' + ); assert.strictEqual(answer2, false); }); }); describe('isIgnored()', () => { - beforeEach(() => { HttpPlugin.isSatisfyPattern = sinon.spy(); }); From 1c73554fb239661e315e2c29f938c9d1975d1813 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Mon, 5 Aug 2019 18:32:06 -0400 Subject: [PATCH 03/18] fix: wip revert - gts fix issue on all packages Signed-off-by: Olivier Albertini --- .../src/BasicTracer.ts | 9 +-- .../src/NodeTracer.ts | 17 ++++- .../test/NodeTracer.test.ts | 74 ++++++++++++++----- .../opentelemetry-plugin-http/src/http.ts | 45 +++++------ 4 files changed, 94 insertions(+), 51 deletions(-) diff --git a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts index afa09ada98..9e375c20cc 100644 --- a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts +++ b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts @@ -118,10 +118,7 @@ export class BasicTracer implements types.Tracer { /** * Enters the scope of code where the given Span is in the current context. */ - withSpan ReturnType>( - span: types.Span, - fn: T - ): ReturnType { + withSpan ReturnType>(span: types.Span, fn: T): ReturnType { // Set given span to context. return this._scopeManager.with(span, fn); } @@ -155,9 +152,7 @@ export class BasicTracer implements types.Tracer { return this._httpTextFormat; } - private _getParentSpanContext( - parent: types.Span | types.SpanContext | undefined - ): types.SpanContext | undefined { + private _getParentSpanContext(parent: types.Span | types.SpanContext | undefined): types.SpanContext | undefined { if (!parent) return undefined; // parent is a SpanContext diff --git a/packages/opentelemetry-node-tracer/src/NodeTracer.ts b/packages/opentelemetry-node-tracer/src/NodeTracer.ts index ea138470e5..4b4f504f92 100644 --- a/packages/opentelemetry-node-tracer/src/NodeTracer.ts +++ b/packages/opentelemetry-node-tracer/src/NodeTracer.ts @@ -25,10 +25,21 @@ export class NodeTracer extends BasicTracer { * Constructs a new Tracer instance. */ constructor(config: BasicTracerConfig) { - super( - Object.assign({}, { scopeManager: new AsyncHooksScopeManager() }, config) - ); + super(Object.assign({}, { scopeManager: new AsyncHooksScopeManager() }, config)); // @todo: Integrate Plugin Loader (pull/126). } + /** + * Binds the trace context to the given event emitter. + * This is necessary in order to create child spans correctly in event + * handlers. + * @param emitter An event emitter whose handlers should have + * the trace context binded to them. + */ + wrapEmitter(emitter: NodeJS.EventEmitter): void { + if (!this._scopeManager.active()) { + return; + } + this._scopeManager.bind(emitter); + } } diff --git a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts index c84fa6a5ab..d5339b5094 100644 --- a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts +++ b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts @@ -21,16 +21,17 @@ import { HttpTraceContext, NEVER_SAMPLER, NoopLogger, - NOOP_SPAN, + NOOP_SPAN } from '@opentelemetry/core'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { NodeTracer } from '../src/NodeTracer'; +import { EventEmitter } from 'events'; describe('NodeTracer', () => { describe('constructor', () => { it('should construct an instance with required only options', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.ok(tracer instanceof NodeTracer); }); @@ -38,7 +39,7 @@ describe('NodeTracer', () => { it('should construct an instance with binary format', () => { const tracer = new NodeTracer({ binaryFormat: new BinaryTraceContext(), - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.ok(tracer instanceof NodeTracer); }); @@ -46,7 +47,7 @@ describe('NodeTracer', () => { it('should construct an instance with http text format', () => { const tracer = new NodeTracer({ httpTextFormat: new HttpTraceContext(), - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.ok(tracer instanceof NodeTracer); }); @@ -54,7 +55,7 @@ describe('NodeTracer', () => { it('should construct an instance with logger', () => { const tracer = new NodeTracer({ logger: new NoopLogger(), - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.ok(tracer instanceof NodeTracer); }); @@ -62,7 +63,7 @@ describe('NodeTracer', () => { it('should construct an instance with sampler', () => { const tracer = new NodeTracer({ scopeManager: new AsyncHooksScopeManager(), - sampler: ALWAYS_SAMPLER, + sampler: ALWAYS_SAMPLER }); assert.ok(tracer instanceof NodeTracer); }); @@ -71,9 +72,9 @@ describe('NodeTracer', () => { const tracer = new NodeTracer({ defaultAttributes: { region: 'eu-west', - asg: 'my-asg', + asg: 'my-asg' }, - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.ok(tracer instanceof NodeTracer); }); @@ -82,7 +83,7 @@ describe('NodeTracer', () => { describe('.startSpan()', () => { it('should start a span with name only', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); const span = tracer.startSpan('my-span'); assert.ok(span); @@ -90,7 +91,7 @@ describe('NodeTracer', () => { it('should start a span with name and options', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); const span = tracer.startSpan('my-span', {}); assert.ok(span); @@ -99,7 +100,7 @@ describe('NodeTracer', () => { it('should return a default span with no sampling', () => { const tracer = new NodeTracer({ sampler: NEVER_SAMPLER, - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); const span = tracer.startSpan('my-span'); assert.deepStrictEqual(span, NOOP_SPAN); @@ -115,7 +116,7 @@ describe('NodeTracer', () => { describe('.getCurrentSpan()', () => { it('should return null with AsyncHooksScopeManager when no span started', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.deepStrictEqual(tracer.getCurrentSpan(), null); }); @@ -124,7 +125,7 @@ describe('NodeTracer', () => { describe('.withSpan()', () => { it('should run scope with AsyncHooksScopeManager scope manager', done => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); const span = tracer.startSpan('my-span'); tracer.withSpan(span, () => { @@ -136,7 +137,7 @@ describe('NodeTracer', () => { it('should run scope with AsyncHooksScopeManager scope manager with multiple spans', done => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); const span = tracer.startSpan('my-span'); tracer.withSpan(span, () => { @@ -145,10 +146,7 @@ describe('NodeTracer', () => { const span1 = tracer.startSpan('my-span1', { parent: span }); tracer.withSpan(span1, () => { assert.deepStrictEqual(tracer.getCurrentSpan(), span1); - assert.deepStrictEqual( - span1.context().traceId, - span.context().traceId - ); + assert.deepStrictEqual(span1.context().traceId, span.context().traceId); return done(); }); }); @@ -181,7 +179,7 @@ describe('NodeTracer', () => { describe('getBinaryFormat', () => { it('should get default binary formatter', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.ok(tracer.getBinaryFormat() instanceof BinaryTraceContext); }); @@ -190,9 +188,45 @@ describe('NodeTracer', () => { describe('.getHttpTextFormat()', () => { it('should get default HTTP text formatter', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), + scopeManager: new AsyncHooksScopeManager() }); assert.ok(tracer.getHttpTextFormat() instanceof HttpTraceContext); }); }); + describe('.wrapEmitter()', () => { + it('should not throw', () => { + const tracer = new NodeTracer({ + scopeManager: new AsyncHooksScopeManager() + }); + tracer.wrapEmitter({} as EventEmitter); + }); + // TODO: uncomment once https://github.com/open-telemetry/opentelemetry-js/pull/146 is merged + // it('should get current Span', (done) => { + // const tracer = new NodeTracer({ + // scopeManager: new AsyncHooksScopeManager(), + // }); + + // const span = tracer.startSpan('my-span'); + // class FakeEventEmitter extends EventEmitter { + // constructor() { + // super() + // } + // test() { + // this.emit('event'); + // } + // } + + // tracer.withSpan(span, () => { + // const fake = new FakeEventEmitter(); + // tracer.wrapEmitter(fake); + // fake.on('event', () => { + // setTimeout(() => { + // assert.deepStrictEqual(tracer.getCurrentSpan(), span); + // done(); + // }, 100); + // }); + // fake.test(); + // }); + // }); + }); }); diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 02c49f8ee0..1d2773d810 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -151,16 +151,16 @@ export class HttpPlugin extends BasePlugin { protected patch() { this._logger.debug('applying patch to %s@%s', this.moduleName, this.version); - shimmer.wrap(this._moduleExports, 'request', this.getPatchOutgoingRequestFunction()); + shimmer.wrap(this._moduleExports, 'request', this._getPatchOutgoingRequestFunction()); // In Node 8-10, http.get calls a private request method, therefore we patch it // here too. if (semver.satisfies(this.version, '>=8.0.0')) { - shimmer.wrap(this._moduleExports, 'get', this.getPatchOutgoingGetFunction()); + shimmer.wrap(this._moduleExports, 'get', this._getPatchOutgoingGetFunction()); } if (this._moduleExports && this._moduleExports.Server && this._moduleExports.Server.prototype) { - shimmer.wrap(this._moduleExports.Server.prototype, 'emit', this.getPatchIncomingRequestFunction()); + 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); } @@ -182,7 +182,7 @@ export class HttpPlugin extends BasePlugin { /** * Creates spans for incoming requests, restoring spans' context if applied. */ - protected getPatchIncomingRequestFunction() { + protected _getPatchIncomingRequestFunction() { return (original: (event: string) => boolean) => { return this.incomingRequestFunction(original, this); }; @@ -192,13 +192,13 @@ export class HttpPlugin extends BasePlugin { * Creates spans for outgoing requests, sending spans' context for distributed * tracing. */ - protected getPatchOutgoingRequestFunction() { + protected _getPatchOutgoingRequestFunction() { return (original: Func): Func => { return this.outgoingRequestFunction(original, this); }; } - protected getPatchOutgoingGetFunction() { + protected _getPatchOutgoingGetFunction() { return (original: Func): Func => { // Re-implement http.get. This needs to be done (instead of using // getPatchOutgoingRequestFunction to patch it) because we need to @@ -249,7 +249,7 @@ export class HttpPlugin extends BasePlugin { propagation.inject(span.context(), HttpPlugin.PROPAGATION_FORMAT, options.headers); request.on('response', (response: IncomingMessage) => { - plugin._tracer.scopeManager.bind(response); + plugin._tracer.wrapEmitter(response); plugin._logger.debug('outgoingRequest on response()'); response.on('end', () => { plugin._logger.debug('outgoingRequest on end()'); @@ -271,8 +271,9 @@ export class HttpPlugin extends BasePlugin { span.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent.toString()); } if (response.statusCode) { - span.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()); - span.setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); + span + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) + .setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); } if (plugin.options.applyCustomAttributesOnSpan) { @@ -322,34 +323,36 @@ export class HttpPlugin extends BasePlugin { const rootSpan = plugin._tracer.startSpan(path, spanOptions); return plugin._tracer.withSpan(rootSpan, () => { - plugin._tracer.scopeManager.bind(request); - plugin._tracer.scopeManager.bind(response); + plugin._tracer.wrapEmitter(request); + plugin._tracer.wrapEmitter(response); // Wraps end (inspired by: // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75) const originalEnd = response.end; response.end = function(this: ServerResponse, ...args: ResponseEndArgs) { response.end = originalEnd; - // Cannot pass args of type ResponseEndArgs, Expected 1-2 arguments, but got 1 or more. + // Cannot pass args of type ResponseEndArgs, + // tslint complains "Expected 1-2 arguments, but got 1 or more.", it does not make sense to me // tslint:disable-next-line:no-any const returned = response.end.apply(this, arguments as any); const requestUrl = request.url ? url.parse(request.url) : null; const host = headers.host || 'localhost'; const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; - rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')); - - rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, method); + rootSpan + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')) + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, method); if (requestUrl) { - rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, requestUrl.pathname || ''); - rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, requestUrl.path || ''); + rootSpan + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, requestUrl.pathname || '') + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, requestUrl.path || ''); } if (userAgent) { rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent); } - rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()); - - rootSpan.setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); + rootSpan + .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) + .setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); if (plugin.options.applyCustomAttributesOnSpan) { plugin.options.applyCustomAttributesOnSpan(rootSpan, request, response); @@ -398,7 +401,7 @@ export class HttpPlugin extends BasePlugin { return request; } - plugin._tracer.scopeManager.bind(request); + plugin._tracer.wrapEmitter(request); if (!method) { method = 'GET'; From 6a864fceaa1ed9ebd548bf2106ee354edaa23b8b Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Mon, 5 Aug 2019 19:49:00 -0400 Subject: [PATCH 04/18] fix: integrate mayurkale22 recommendations Signed-off-by: Olivier Albertini --- packages/opentelemetry-plugin-http/.DS_Store | Bin 6148 -> 0 bytes .../opentelemetry-plugin-http/package.json | 2 +- .../src/enums/attributes.ts | 17 ++ .../src/enums/format.ts | 4 + .../opentelemetry-plugin-http/src/http.ts | 222 ++++-------------- .../opentelemetry-plugin-http/src/utils.ts | 116 +++++++++ ...p-static-methods.test.ts => utils.test.ts} | 48 ++-- 7 files changed, 214 insertions(+), 195 deletions(-) delete mode 100644 packages/opentelemetry-plugin-http/.DS_Store create mode 100644 packages/opentelemetry-plugin-http/src/enums/attributes.ts create mode 100644 packages/opentelemetry-plugin-http/src/enums/format.ts create mode 100644 packages/opentelemetry-plugin-http/src/utils.ts rename packages/opentelemetry-plugin-http/test/{http-static-methods.test.ts => utils.test.ts} (66%) diff --git a/packages/opentelemetry-plugin-http/.DS_Store b/packages/opentelemetry-plugin-http/.DS_Store deleted file mode 100644 index a2eb533abf551d042629249e6fe5fb378b3b5ee0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~u?oUK42Bc!Ah>jNyu}Cb4Gz&K=nFU~E>c0O^F6wMazU^xC+1b{XuyJ79K1T}(S!u1*@b}wNMJ-@TJzTK|1JE}{6A`8N&+PC zX9Tp_belC^D(=>|*R%RAs { - /** - * Attributes Names according to Opencensus HTTP Specs since there is no specific OpenTelemetry Attributes - * https://github.com/open-telemetry/opentelemetry-specification/blob/master/work_in_progress/opencensus/HTTP.md#attributes - */ - static ATTRIBUTE_HTTP_HOST = 'http.host'; - // NOT ON OFFICIAL SPEC - static ATTRIBUTE_ERROR = 'error'; - 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'; - // NOT ON OFFICIAL SPEC - static ATTRIBUTE_HTTP_ERROR_NAME = 'http.error_name'; - static ATTRIBUTE_HTTP_ERROR_MESSAGE = 'http.error_message'; - static PROPAGATION_FORMAT = 'HttpTraceContext'; - - /** - * Parse status code from HTTP response. - */ - static parseResponseStatus(statusCode: number): Omit { - if (statusCode < 200 || statusCode > 504) { - return { code: CanonicalCode.UNKNOWN }; - } else if (statusCode >= 200 && statusCode < 400) { - return { code: CanonicalCode.OK }; - } else { - switch (statusCode) { - case 400: - return { code: CanonicalCode.INVALID_ARGUMENT }; - case 504: - return { code: CanonicalCode.DEADLINE_EXCEEDED }; - case 404: - return { code: CanonicalCode.NOT_FOUND }; - case 403: - return { code: CanonicalCode.PERMISSION_DENIED }; - case 401: - return { code: CanonicalCode.UNAUTHENTICATED }; - case 429: - return { code: CanonicalCode.RESOURCE_EXHAUSTED }; - case 501: - return { code: CanonicalCode.UNIMPLEMENTED }; - case 503: - return { code: CanonicalCode.UNAVAILABLE }; - default: - return { code: CanonicalCode.UNKNOWN }; - } - } - } - - /** - * Returns whether the Expect header is on the given options object. - * @param options Options for http.request. - */ - static hasExpectHeader(options: RequestOptions | url.URL): boolean { - return !!((options as RequestOptions).headers && (options as RequestOptions).headers!.Expect); - } - - /** - * Check whether the given request match pattern - * @param url URL of request - * @param request Request to inspect - * @param pattern Match pattern - */ - static isSatisfyPattern(url: string, request: T, pattern: IgnoreMatcher): boolean { - 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'); - } - } - - /** - * Check whether the given request is ignored by configuration - * @param url URL of request - * @param request Request to inspect - * @param list List of ignore patterns - */ - static isIgnored(url: string, request: T, list?: Array>): boolean { - if (!list) { - // No ignored urls - trace everything - return false; - } - - for (const pattern of list) { - if (HttpPlugin.isSatisfyPattern(url, request, pattern)) { - return true; - } - } - - return false; - } - - static setSpanOnError(span: Span, obj: NodeJS.EventEmitter) { - obj.on('error', error => { - span - .setAttribute(HttpPlugin.ATTRIBUTE_ERROR, true) - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_NAME, error.name) - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_MESSAGE, error.message); - span.setStatus({ code: CanonicalCode.UNKNOWN, message: error.message }); - span.end(); - }); - } - options!: HttpPluginConfig; // TODO: BasePlugin should pass the logger or when we enable the plugin @@ -184,7 +80,7 @@ export class HttpPlugin extends BasePlugin { */ protected _getPatchIncomingRequestFunction() { return (original: (event: string) => boolean) => { - return this.incomingRequestFunction(original, this); + return this.incomingRequestFunction(original); }; } @@ -194,7 +90,7 @@ export class HttpPlugin extends BasePlugin { */ protected _getPatchOutgoingRequestFunction() { return (original: Func): Func => { - return this.outgoingRequestFunction(original, this); + return this.outgoingRequestFunction(original); }; } @@ -219,80 +115,74 @@ export class HttpPlugin extends BasePlugin { } /** - * Injects span's context to header for distributed tracing and finshes the + * Injects span's context to header for distributed tracing and finishes the * span when the response is finished. - * @param original The original patched function. + * @param request The original request object. * @param options The arguments to the original function. */ - private getMakeRequestTraceFunction( - request: ClientRequest, - options: RequestOptions, - plugin: HttpPlugin - ): Func { + private getMakeRequestTraceFunction(request: ClientRequest, options: RequestOptions): Func { return (span: Span): ClientRequest => { - plugin._logger.debug('makeRequestTrace'); + this._logger.debug('makeRequestTrace'); if (!span) { - plugin._logger.debug('makeRequestTrace span is null'); + this._logger.debug('makeRequestTrace span is null'); return request; } - const propagation = plugin._tracer.getHttpTextFormat(); + const propagation = this._tracer.getHttpTextFormat(); // If outgoing request headers contain the "Expect" header, the returned // ClientRequest will throw an error if any new headers are added. // So we need to clone the options object to be able to inject new // header. - if (HttpPlugin.hasExpectHeader(options)) { + if (Utils.hasExpectHeader(options)) { options = Object.assign({}, options); options.headers = Object.assign({}, options.headers); } - propagation.inject(span.context(), HttpPlugin.PROPAGATION_FORMAT, options.headers); + propagation.inject(span.context(), Format.HTTP, options.headers); request.on('response', (response: IncomingMessage) => { - plugin._tracer.wrapEmitter(response); - plugin._logger.debug('outgoingRequest on response()'); + this._tracer.wrapEmitter(response); + this._logger.debug('outgoingRequest on response()'); response.on('end', () => { - plugin._logger.debug('outgoingRequest on end()'); + this._logger.debug('outgoingRequest on end()'); const method = response.method ? response.method.toUpperCase() : 'GET'; const headers = options.headers; const userAgent = headers ? headers['user-agent'] || headers['User-Agent'] : null; const host = options.hostname || options.host || 'localhost'; - span - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host) - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, method); - if (options.path) { - span - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, options.path) - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, options.path); - } + span.setAttributes({ + ATTRIBUTE_HTTP_HOST: host, + ATTRIBUTE_HTTP_METHOD: method, + ATTRIBUTE_HTTP_PATH: options.path || '/' + }); if (userAgent) { - span.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent.toString()); + span.setAttribute(Attributes.ATTRIBUTE_HTTP_USER_AGENT, userAgent); } if (response.statusCode) { span - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) - .setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); + .setAttribute(Attributes.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) + .setStatus(Utils.parseResponseStatus(response.statusCode)); } - if (plugin.options.applyCustomAttributesOnSpan) { - plugin.options.applyCustomAttributesOnSpan(span, request, response); + if (this.options.applyCustomAttributesOnSpan) { + this.options.applyCustomAttributesOnSpan(span, request, response); } span.end(); }); - HttpPlugin.setSpanOnError(span, response); + Utils.setSpanOnError(span, response); }); - HttpPlugin.setSpanOnError(span, request); + Utils.setSpanOnError(span, request); - plugin._logger.debug('makeRequestTrace return request'); + this._logger.debug('makeRequestTrace return request'); return request; }; } - private incomingRequestFunction(original: (event: string, ...args: unknown[]) => boolean, plugin: HttpPlugin) { + private incomingRequestFunction(original: (event: string, ...args: unknown[]) => boolean) { + const plugin = this; return function incomingRequest(this: {}, event: string, ...args: unknown[]): boolean { // Only traces request events if (event !== 'request') { @@ -305,7 +195,7 @@ export class HttpPlugin extends BasePlugin { const method = request.method || 'GET'; plugin._logger.debug('%s plugin incomingRequest', plugin.moduleName); - if (HttpPlugin.isIgnored(path, request, plugin.options.ignoreIncomingPaths)) { + if (Utils.isIgnored(path, request, plugin.options.ignoreIncomingPaths)) { return original.apply(this, [event, ...args]); } @@ -316,7 +206,7 @@ export class HttpPlugin extends BasePlugin { kind: SpanKind.SERVER }; - const spanContext = propagation.extract(HttpPlugin.PROPAGATION_FORMAT, headers); + const spanContext = propagation.extract(Format.HTTP, headers); if (spanContext && isValid(spanContext)) { spanOptions.parent = spanContext; } @@ -340,19 +230,20 @@ export class HttpPlugin extends BasePlugin { const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; rootSpan - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')) - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, method); + .setAttribute(Attributes.ATTRIBUTE_HTTP_HOST, host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')) + .setAttribute(Attributes.ATTRIBUTE_HTTP_METHOD, method); + if (requestUrl) { rootSpan - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, requestUrl.pathname || '') - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, requestUrl.path || ''); + .setAttribute(Attributes.ATTRIBUTE_HTTP_PATH, requestUrl.pathname || '/') + .setAttribute(Attributes.ATTRIBUTE_HTTP_ROUTE, requestUrl.path || '/'); } if (userAgent) { - rootSpan.setAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent); + rootSpan.setAttribute(Attributes.ATTRIBUTE_HTTP_USER_AGENT, userAgent); } rootSpan - .setAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) - .setStatus(HttpPlugin.parseResponseStatus(response.statusCode)); + .setAttribute(Attributes.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) + .setStatus(Utils.parseResponseStatus(response.statusCode)); if (plugin.options.applyCustomAttributesOnSpan) { plugin.options.applyCustomAttributesOnSpan(rootSpan, request, response); @@ -366,7 +257,8 @@ export class HttpPlugin extends BasePlugin { }; } - private outgoingRequestFunction(original: Func, plugin: HttpPlugin): Func { + private outgoingRequestFunction(original: Func): Func { + const plugin = this; return function outgoingRequest( this: {}, options: RequestOptions | string, @@ -378,7 +270,6 @@ export class HttpPlugin extends BasePlugin { // Makes sure the url is an url object let pathname; - let method; let origin = ''; if (typeof options === 'string') { const parsedUrl = url.parse(options); @@ -391,27 +282,18 @@ export class HttpPlugin extends BasePlugin { if (!pathname) { pathname = options.path ? url.parse(options.path).pathname : ''; } - method = options.method; origin = `${options.protocol || 'http:'}//${options.host}`; } catch (ignore) {} } const request: ClientRequest = original.apply(this, [options, callback]); - - if (HttpPlugin.isIgnored(origin + pathname, request, plugin.options.ignoreOutgoingUrls)) { + if (Utils.isIgnored(origin + pathname, request, plugin.options.ignoreOutgoingUrls)) { return request; } - - plugin._tracer.wrapEmitter(request); - - if (!method) { - method = 'GET'; - } else { - // some packages return method in lowercase.. - // ensure upperCase for consistency - method = method.toUpperCase(); - } - plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); + plugin._tracer.wrapEmitter(request); + // some packages return method in lowercase.. + // ensure upperCase for consistency + const method = options.method ? options.method.toUpperCase() : 'GET'; const operationName = `${method} ${pathname}`; const spanOptions = { kind: SpanKind.CLIENT @@ -424,14 +306,14 @@ export class HttpPlugin extends BasePlugin { if (!currentSpan) { plugin._logger.debug('outgoingRequest starting a root span'); const rootSpan = plugin._tracer.startSpan(operationName, spanOptions); - return plugin._tracer.withSpan(rootSpan, plugin.getMakeRequestTraceFunction(request, options, plugin)); + return plugin._tracer.withSpan(rootSpan, plugin.getMakeRequestTraceFunction(request, options)); } else { plugin._logger.debug('outgoingRequest starting a child span'); const span = plugin._tracer.startSpan(operationName, { kind: spanOptions.kind, parent: currentSpan }); - return plugin.getMakeRequestTraceFunction(request, options, plugin)(span); + return plugin.getMakeRequestTraceFunction(request, options)(span); } }; } diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts new file mode 100644 index 0000000000..179a1656ee --- /dev/null +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -0,0 +1,116 @@ +import { Status, CanonicalCode, Span } from '@opentelemetry/types'; +import { RequestOptions, IncomingMessage, ClientRequest } from 'http'; +import { IgnoreMatcher } from './types'; +import { Attributes } from './enums/Attributes'; +import * as url from 'url'; + +/** + * Utility class + */ +export class Utils { + /** + * Parse status code from HTTP response. + */ + static parseResponseStatus(statusCode: number): Omit { + if (statusCode < 200 || statusCode > 504) { + return { code: CanonicalCode.UNKNOWN }; + } else if (statusCode >= 200 && statusCode < 400) { + return { code: CanonicalCode.OK }; + } else { + switch (statusCode) { + case 400: + return { code: CanonicalCode.INVALID_ARGUMENT }; + case 504: + return { code: CanonicalCode.DEADLINE_EXCEEDED }; + case 404: + return { code: CanonicalCode.NOT_FOUND }; + case 403: + return { code: CanonicalCode.PERMISSION_DENIED }; + case 401: + return { code: CanonicalCode.UNAUTHENTICATED }; + case 429: + return { code: CanonicalCode.RESOURCE_EXHAUSTED }; + case 501: + return { code: CanonicalCode.UNIMPLEMENTED }; + case 503: + return { code: CanonicalCode.UNAVAILABLE }; + default: + return { code: CanonicalCode.UNKNOWN }; + } + } + } + + /** + * Returns whether the Expect header is on the given options object. + * @param options Options for http.request. + */ + static hasExpectHeader(options: RequestOptions | url.URL): boolean { + return !!((options as RequestOptions).headers && (options as RequestOptions).headers!.Expect); + } + + /** + * Check whether the given obj match pattern + * @param constant e.g URL of request + * @param obj obj to inspect + * @param pattern Match pattern + */ + static isSatisfyPattern(constant: string, obj: T, pattern: IgnoreMatcher): boolean { + if (typeof pattern === 'string') { + return pattern === constant; + } else if (pattern instanceof RegExp) { + return pattern.test(constant); + } else if (typeof pattern === 'function') { + return pattern(constant, obj); + } else { + throw new TypeError('Pattern is in unsupported datatype'); + } + } + + /** + * Check whether the given request is ignored by configuration + * @param constant e.g URL of request + * @param obj obj to inspect + * @param list List of ignore patterns + */ + static isIgnored(constant: string, obj: T, list?: Array>): boolean { + if (!list) { + // No ignored urls - trace everything + return false; + } + + for (const pattern of list) { + if (Utils.isSatisfyPattern(constant, obj, pattern)) { + return true; + } + } + + return false; + } + + /** + * Will subscribe obj on error event and will set attributes when emitting event + * @param span to set + * @param obj to subscribe on error + */ + static setSpanOnError(span: Span, obj: IncomingMessage | ClientRequest) { + obj.on('error', error => { + span.setAttributes({ + [Attributes.ATTRIBUTE_ERROR]: true, + [Attributes.ATTRIBUTE_HTTP_ERROR_NAME]: error.name, + [Attributes.ATTRIBUTE_HTTP_ERROR_MESSAGE]: error.message + }); + + let status: Status; + if ((obj as IncomingMessage).statusCode) { + status = Utils.parseResponseStatus((obj as IncomingMessage).statusCode!); + } else { + status = { code: CanonicalCode.UNKNOWN }; + } + + status.message = error.message; + + span.setStatus(status); + span.end(); + }); + } +} diff --git a/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts similarity index 66% rename from packages/opentelemetry-plugin-http/test/http-static-methods.test.ts rename to packages/opentelemetry-plugin-http/test/utils.test.ts index 00f9c0d339..4c9c1cb68a 100644 --- a/packages/opentelemetry-plugin-http/test/http-static-methods.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -17,27 +17,27 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { CanonicalCode } from '@opentelemetry/types'; -import { HttpPlugin } from '../src/http'; import { RequestOptions } from 'https'; import { IgnoreMatcher } from '../src/types'; +import { Utils } from '../src/utils'; -describe('Static HttpPlugin methods', () => { +describe('Utils', () => { describe('parseResponseStatus()', () => { it('should return UNKNOWN code by default', () => { - const status = HttpPlugin.parseResponseStatus((undefined as unknown) as number); + const status = Utils.parseResponseStatus((undefined as unknown) as number); assert.deepStrictEqual(status, { code: CanonicalCode.UNKNOWN }); }); it('should return OK for Success HTTP status code', () => { for (let index = 200; index < 400; index++) { - const status = HttpPlugin.parseResponseStatus(index); + const status = Utils.parseResponseStatus(index); assert.deepStrictEqual(status, { code: CanonicalCode.OK }); } }); it('should not return OK for Bad HTTP status code', () => { for (let index = 400; index <= 504; index++) { - const status = HttpPlugin.parseResponseStatus(index); + const status = Utils.parseResponseStatus(index); assert.notStrictEqual(status.code, CanonicalCode.OK); } }); @@ -45,39 +45,39 @@ describe('Static HttpPlugin methods', () => { describe('hasExpectHeader()', () => { it('should throw if no option', () => { try { - HttpPlugin.hasExpectHeader('' as RequestOptions); + Utils.hasExpectHeader('' as RequestOptions); assert.fail(); } catch (ignore) {} }); it('should not throw if no headers', () => { - const result = HttpPlugin.hasExpectHeader({} as RequestOptions); + const result = Utils.hasExpectHeader({} as RequestOptions); assert.strictEqual(result, false); }); it('should return true on Expect', () => { - const result = HttpPlugin.hasExpectHeader({ headers: { Expect: 1 } } as RequestOptions); + const result = Utils.hasExpectHeader({ headers: { Expect: 1 } } as RequestOptions); assert.strictEqual(result, true); }); }); describe('isSatisfyPattern()', () => { it('string pattern', () => { - const answer1 = HttpPlugin.isSatisfyPattern('/test/1', {}, '/test/1'); + const answer1 = Utils.isSatisfyPattern('/test/1', {}, '/test/1'); assert.strictEqual(answer1, true); - const answer2 = HttpPlugin.isSatisfyPattern('/test/1', {}, '/test/11'); + const answer2 = Utils.isSatisfyPattern('/test/1', {}, '/test/11'); assert.strictEqual(answer2, false); }); it('regex pattern', () => { - const answer1 = HttpPlugin.isSatisfyPattern('/TeSt/1', {}, /\/test/i); + const answer1 = Utils.isSatisfyPattern('/TeSt/1', {}, /\/test/i); assert.strictEqual(answer1, true); - const answer2 = HttpPlugin.isSatisfyPattern('/2/tEst/1', {}, /\/test/); + const answer2 = Utils.isSatisfyPattern('/2/tEst/1', {}, /\/test/); assert.strictEqual(answer2, false); }); it('should throw if type is unknown', () => { try { - HttpPlugin.isSatisfyPattern('/TeSt/1', {}, (true as unknown) as IgnoreMatcher<{}>); + Utils.isSatisfyPattern('/TeSt/1', {}, (true as unknown) as IgnoreMatcher<{}>); assert.fail(); } catch (error) { assert.strictEqual(error instanceof TypeError, true); @@ -85,13 +85,13 @@ describe('Static HttpPlugin methods', () => { }); it('function pattern', () => { - const answer1 = HttpPlugin.isSatisfyPattern( + const answer1 = Utils.isSatisfyPattern( '/test/home', { headers: {} }, (url: string, req: { headers: unknown }) => req.headers && url === '/test/home' ); assert.strictEqual(answer1, true); - const answer2 = HttpPlugin.isSatisfyPattern( + const answer2 = Utils.isSatisfyPattern( '/test/home', { headers: {} }, (url: string, req: { headers: unknown }) => url !== '/test/home' @@ -102,7 +102,7 @@ describe('Static HttpPlugin methods', () => { describe('isIgnored()', () => { beforeEach(() => { - HttpPlugin.isSatisfyPattern = sinon.spy(); + Utils.isSatisfyPattern = sinon.spy(); }); afterEach(() => { @@ -110,29 +110,29 @@ describe('Static HttpPlugin methods', () => { }); it('should call isSatisfyPattern, n match', () => { - const answer1 = HttpPlugin.isIgnored('/test/1', {}, ['/test/11']); + const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); - assert.strictEqual((HttpPlugin.isSatisfyPattern as sinon.SinonSpy).callCount, 1); + assert.strictEqual((Utils.isSatisfyPattern as sinon.SinonSpy).callCount, 1); }); it('should call isSatisfyPattern, match', () => { - const answer1 = HttpPlugin.isIgnored('/test/1', {}, ['/test/11']); + const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); - assert.strictEqual((HttpPlugin.isSatisfyPattern as sinon.SinonSpy).callCount, 1); + assert.strictEqual((Utils.isSatisfyPattern as sinon.SinonSpy).callCount, 1); }); it('should not call isSatisfyPattern', () => { - HttpPlugin.isIgnored('/test/1', {}, []); - assert.strictEqual((HttpPlugin.isSatisfyPattern as sinon.SinonSpy).callCount, 0); + Utils.isIgnored('/test/1', {}, []); + assert.strictEqual((Utils.isSatisfyPattern as sinon.SinonSpy).callCount, 0); }); it('should return false on empty list', () => { - const answer1 = HttpPlugin.isIgnored('/test/1', {}, []); + const answer1 = Utils.isIgnored('/test/1', {}, []); assert.strictEqual(answer1, false); }); it('should not throw and return false when list is undefined', () => { - const answer2 = HttpPlugin.isIgnored('/test/1', {}, undefined); + const answer2 = Utils.isIgnored('/test/1', {}, undefined); assert.strictEqual(answer2, false); }); }); From 7a7845d0edc5a3ba7232ea1a14ff0d1e5cd252a0 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Mon, 5 Aug 2019 20:07:28 -0400 Subject: [PATCH 05/18] ci: gts fix for ci build Signed-off-by: Olivier Albertini --- .../src/BasicTracer.ts | 9 +- .../src/NodeTracer.ts | 4 +- .../opentelemetry-node-tracer/src/index.ts | 2 +- .../test/NodeTracer.test.ts | 39 +++-- .../opentelemetry-plugin-http/package.json | 7 +- .../src/enums/attributes.ts | 2 +- .../src/enums/format.ts | 2 +- .../opentelemetry-plugin-http/src/http.ts | 156 ++++++++++++++---- .../opentelemetry-plugin-http/src/types.ts | 19 ++- .../opentelemetry-plugin-http/src/utils.ts | 25 ++- .../test/http-disable.test.ts | 20 ++- .../test/http-enable.test.ts | 8 +- .../test/utils.test.ts | 32 +++- 13 files changed, 242 insertions(+), 83 deletions(-) diff --git a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts index 9e375c20cc..afa09ada98 100644 --- a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts +++ b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts @@ -118,7 +118,10 @@ export class BasicTracer implements types.Tracer { /** * Enters the scope of code where the given Span is in the current context. */ - withSpan ReturnType>(span: types.Span, fn: T): ReturnType { + withSpan ReturnType>( + span: types.Span, + fn: T + ): ReturnType { // Set given span to context. return this._scopeManager.with(span, fn); } @@ -152,7 +155,9 @@ export class BasicTracer implements types.Tracer { return this._httpTextFormat; } - private _getParentSpanContext(parent: types.Span | types.SpanContext | undefined): types.SpanContext | undefined { + private _getParentSpanContext( + parent: types.Span | types.SpanContext | undefined + ): types.SpanContext | undefined { if (!parent) return undefined; // parent is a SpanContext diff --git a/packages/opentelemetry-node-tracer/src/NodeTracer.ts b/packages/opentelemetry-node-tracer/src/NodeTracer.ts index 4b4f504f92..ff822d0067 100644 --- a/packages/opentelemetry-node-tracer/src/NodeTracer.ts +++ b/packages/opentelemetry-node-tracer/src/NodeTracer.ts @@ -25,7 +25,9 @@ export class NodeTracer extends BasicTracer { * Constructs a new Tracer instance. */ constructor(config: BasicTracerConfig) { - super(Object.assign({}, { scopeManager: new AsyncHooksScopeManager() }, config)); + super( + Object.assign({}, { scopeManager: new AsyncHooksScopeManager() }, config) + ); // @todo: Integrate Plugin Loader (pull/126). } diff --git a/packages/opentelemetry-node-tracer/src/index.ts b/packages/opentelemetry-node-tracer/src/index.ts index 310d6a16a9..0478f974a4 100644 --- a/packages/opentelemetry-node-tracer/src/index.ts +++ b/packages/opentelemetry-node-tracer/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ - export * from './NodeTracer'; +export * from './NodeTracer'; diff --git a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts index d5339b5094..2bbbd22152 100644 --- a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts +++ b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts @@ -21,7 +21,7 @@ import { HttpTraceContext, NEVER_SAMPLER, NoopLogger, - NOOP_SPAN + NOOP_SPAN, } from '@opentelemetry/core'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { NodeTracer } from '../src/NodeTracer'; @@ -31,7 +31,7 @@ describe('NodeTracer', () => { describe('constructor', () => { it('should construct an instance with required only options', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.ok(tracer instanceof NodeTracer); }); @@ -39,7 +39,7 @@ describe('NodeTracer', () => { it('should construct an instance with binary format', () => { const tracer = new NodeTracer({ binaryFormat: new BinaryTraceContext(), - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.ok(tracer instanceof NodeTracer); }); @@ -47,7 +47,7 @@ describe('NodeTracer', () => { it('should construct an instance with http text format', () => { const tracer = new NodeTracer({ httpTextFormat: new HttpTraceContext(), - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.ok(tracer instanceof NodeTracer); }); @@ -55,7 +55,7 @@ describe('NodeTracer', () => { it('should construct an instance with logger', () => { const tracer = new NodeTracer({ logger: new NoopLogger(), - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.ok(tracer instanceof NodeTracer); }); @@ -63,7 +63,7 @@ describe('NodeTracer', () => { it('should construct an instance with sampler', () => { const tracer = new NodeTracer({ scopeManager: new AsyncHooksScopeManager(), - sampler: ALWAYS_SAMPLER + sampler: ALWAYS_SAMPLER, }); assert.ok(tracer instanceof NodeTracer); }); @@ -72,9 +72,9 @@ describe('NodeTracer', () => { const tracer = new NodeTracer({ defaultAttributes: { region: 'eu-west', - asg: 'my-asg' + asg: 'my-asg', }, - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.ok(tracer instanceof NodeTracer); }); @@ -83,7 +83,7 @@ describe('NodeTracer', () => { describe('.startSpan()', () => { it('should start a span with name only', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); const span = tracer.startSpan('my-span'); assert.ok(span); @@ -91,7 +91,7 @@ describe('NodeTracer', () => { it('should start a span with name and options', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); const span = tracer.startSpan('my-span', {}); assert.ok(span); @@ -100,7 +100,7 @@ describe('NodeTracer', () => { it('should return a default span with no sampling', () => { const tracer = new NodeTracer({ sampler: NEVER_SAMPLER, - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); const span = tracer.startSpan('my-span'); assert.deepStrictEqual(span, NOOP_SPAN); @@ -116,7 +116,7 @@ describe('NodeTracer', () => { describe('.getCurrentSpan()', () => { it('should return null with AsyncHooksScopeManager when no span started', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.deepStrictEqual(tracer.getCurrentSpan(), null); }); @@ -125,7 +125,7 @@ describe('NodeTracer', () => { describe('.withSpan()', () => { it('should run scope with AsyncHooksScopeManager scope manager', done => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); const span = tracer.startSpan('my-span'); tracer.withSpan(span, () => { @@ -137,7 +137,7 @@ describe('NodeTracer', () => { it('should run scope with AsyncHooksScopeManager scope manager with multiple spans', done => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); const span = tracer.startSpan('my-span'); tracer.withSpan(span, () => { @@ -146,7 +146,10 @@ describe('NodeTracer', () => { const span1 = tracer.startSpan('my-span1', { parent: span }); tracer.withSpan(span1, () => { assert.deepStrictEqual(tracer.getCurrentSpan(), span1); - assert.deepStrictEqual(span1.context().traceId, span.context().traceId); + assert.deepStrictEqual( + span1.context().traceId, + span.context().traceId + ); return done(); }); }); @@ -179,7 +182,7 @@ describe('NodeTracer', () => { describe('getBinaryFormat', () => { it('should get default binary formatter', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.ok(tracer.getBinaryFormat() instanceof BinaryTraceContext); }); @@ -188,7 +191,7 @@ describe('NodeTracer', () => { describe('.getHttpTextFormat()', () => { it('should get default HTTP text formatter', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); assert.ok(tracer.getHttpTextFormat() instanceof HttpTraceContext); }); @@ -196,7 +199,7 @@ describe('NodeTracer', () => { describe('.wrapEmitter()', () => { it('should not throw', () => { const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager() + scopeManager: new AsyncHooksScopeManager(), }); tracer.wrapEmitter({} as EventEmitter); }); diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index 9db83ca6d9..d5a04d7980 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -38,20 +38,19 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/scope-async-hooks": "^0.0.1", "@types/mocha": "^5.2.7", "@types/nock": "^10.0.3", "@types/node": "^12.6.9", "@types/semver": "^6.0.1", "@types/shimmer": "^1.0.1", "@types/sinon":"^7.0.13", + "@opentelemetry/scope-async-hooks": "^0.0.1", "codecov": "^3.5.0", - "gts": "^1.1.0", + "gts": "^1.0.0", "mocha": "^6.2.0", + "nock": "^10.0.6", "nyc": "^14.1.1", "sinon": "^7.3.2", - "c8": "^5.0.1", - "nock": "^10.0.6", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", "typescript": "^3.5.3" diff --git a/packages/opentelemetry-plugin-http/src/enums/attributes.ts b/packages/opentelemetry-plugin-http/src/enums/attributes.ts index 2e7930539b..4f2d308032 100644 --- a/packages/opentelemetry-plugin-http/src/enums/attributes.ts +++ b/packages/opentelemetry-plugin-http/src/enums/attributes.ts @@ -13,5 +13,5 @@ export enum Attributes { ATTRIBUTE_HTTP_STATUS_CODE = 'http.status_code', // NOT ON OFFICIAL SPEC ATTRIBUTE_HTTP_ERROR_NAME = 'http.error_name', - ATTRIBUTE_HTTP_ERROR_MESSAGE = 'http.error_message' + ATTRIBUTE_HTTP_ERROR_MESSAGE = 'http.error_message', } diff --git a/packages/opentelemetry-plugin-http/src/enums/format.ts b/packages/opentelemetry-plugin-http/src/enums/format.ts index 23382ae089..962271af44 100644 --- a/packages/opentelemetry-plugin-http/src/enums/format.ts +++ b/packages/opentelemetry-plugin-http/src/enums/format.ts @@ -1,4 +1,4 @@ // Should we export this to the types package in order to expose all values ? export enum Format { - HTTP = 'HttpTraceContext' + HTTP = 'HttpTraceContext', } diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 50e2420158..767520bf7b 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -17,11 +17,23 @@ import { BasePlugin, NoopLogger, isValid } from '@opentelemetry/core'; import { Span, SpanKind, SpanOptions, Logger } from '@opentelemetry/types'; import { NodeTracer } from '@opentelemetry/node-tracer'; -import { ClientRequest, IncomingMessage, request, RequestOptions, ServerResponse } from 'http'; +import { + ClientRequest, + IncomingMessage, + request, + RequestOptions, + ServerResponse, +} from 'http'; import * as semver from 'semver'; import * as shimmer from 'shimmer'; import * as url from 'url'; -import { HttpPluginConfig, Http, Func, HttpCallback, ResponseEndArgs } from './types'; +import { + HttpPluginConfig, + Http, + Func, + HttpCallback, + ResponseEndArgs, +} from './types'; import { Format } from './enums/format'; import { Attributes } from './enums/attributes'; import { Utils } from './utils'; @@ -45,20 +57,43 @@ export class HttpPlugin extends BasePlugin { /** Patches HTTP incoming and outcoming request functions. */ protected patch() { - this._logger.debug('applying patch to %s@%s', this.moduleName, this.version); - - shimmer.wrap(this._moduleExports, 'request', this._getPatchOutgoingRequestFunction()); + this._logger.debug( + 'applying patch to %s@%s', + this.moduleName, + this.version + ); + + shimmer.wrap( + this._moduleExports, + 'request', + this._getPatchOutgoingRequestFunction() + ); // In Node 8-10, http.get calls a private request method, therefore we patch it // here too. if (semver.satisfies(this.version, '>=8.0.0')) { - shimmer.wrap(this._moduleExports, 'get', this._getPatchOutgoingGetFunction()); + shimmer.wrap( + this._moduleExports, + 'get', + this._getPatchOutgoingGetFunction() + ); } - if (this._moduleExports && this._moduleExports.Server && this._moduleExports.Server.prototype) { - shimmer.wrap(this._moduleExports.Server.prototype, 'emit', this._getPatchIncomingRequestFunction()); + 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); + this._logger.error( + 'Could not apply patch to %s.emit. Interface is not as expected.', + this.moduleName + ); } return this._moduleExports; @@ -70,7 +105,11 @@ export class HttpPlugin extends BasePlugin { if (semver.satisfies(this.version, '>=8.0.0')) { shimmer.unwrap(this._moduleExports, 'get'); } - if (this._moduleExports && this._moduleExports.Server && this._moduleExports.Server.prototype) { + if ( + this._moduleExports && + this._moduleExports.Server && + this._moduleExports.Server.prototype + ) { shimmer.unwrap(this._moduleExports.Server.prototype, 'emit'); } } @@ -106,7 +145,10 @@ export class HttpPlugin extends BasePlugin { // simply follow the latter. Ref: // https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback // https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/plugins/plugin-http.ts#L198 - return function outgoingGetRequest(options: string | RequestOptions | URL, callback?: HttpCallback) { + return function outgoingGetRequest( + options: string | RequestOptions | URL, + callback?: HttpCallback + ) { const req = request(options, callback); req.end(); return req; @@ -120,7 +162,10 @@ export class HttpPlugin extends BasePlugin { * @param request The original request object. * @param options The arguments to the original function. */ - private getMakeRequestTraceFunction(request: ClientRequest, options: RequestOptions): Func { + private getMakeRequestTraceFunction( + request: ClientRequest, + options: RequestOptions + ): Func { return (span: Span): ClientRequest => { this._logger.debug('makeRequestTrace'); @@ -145,15 +190,19 @@ export class HttpPlugin extends BasePlugin { this._logger.debug('outgoingRequest on response()'); response.on('end', () => { this._logger.debug('outgoingRequest on end()'); - const method = response.method ? response.method.toUpperCase() : 'GET'; + const method = response.method + ? response.method.toUpperCase() + : 'GET'; const headers = options.headers; - const userAgent = headers ? headers['user-agent'] || headers['User-Agent'] : null; + const userAgent = headers + ? headers['user-agent'] || headers['User-Agent'] + : null; const host = options.hostname || options.host || 'localhost'; span.setAttributes({ ATTRIBUTE_HTTP_HOST: host, ATTRIBUTE_HTTP_METHOD: method, - ATTRIBUTE_HTTP_PATH: options.path || '/' + ATTRIBUTE_HTTP_PATH: options.path || '/', }); if (userAgent) { @@ -161,7 +210,10 @@ export class HttpPlugin extends BasePlugin { } if (response.statusCode) { span - .setAttribute(Attributes.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) + .setAttribute( + Attributes.ATTRIBUTE_HTTP_STATUS_CODE, + response.statusCode.toString() + ) .setStatus(Utils.parseResponseStatus(response.statusCode)); } @@ -181,9 +233,15 @@ export class HttpPlugin extends BasePlugin { }; } - private incomingRequestFunction(original: (event: string, ...args: unknown[]) => boolean) { + private incomingRequestFunction( + original: (event: string, ...args: unknown[]) => boolean + ) { const plugin = this; - return function incomingRequest(this: {}, event: string, ...args: unknown[]): boolean { + return function incomingRequest( + this: {}, + event: string, + ...args: unknown[] + ): boolean { // Only traces request events if (event !== 'request') { return original.apply(this, [event, ...args]); @@ -203,7 +261,7 @@ export class HttpPlugin extends BasePlugin { const headers = request.headers; const spanOptions: SpanOptions = { - kind: SpanKind.SERVER + kind: SpanKind.SERVER, }; const spanContext = propagation.extract(Format.HTTP, headers); @@ -219,7 +277,10 @@ export class HttpPlugin extends BasePlugin { // Wraps end (inspired by: // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75) const originalEnd = response.end; - response.end = function(this: ServerResponse, ...args: ResponseEndArgs) { + response.end = function( + this: ServerResponse, + ...args: ResponseEndArgs + ) { response.end = originalEnd; // Cannot pass args of type ResponseEndArgs, // tslint complains "Expected 1-2 arguments, but got 1 or more.", it does not make sense to me @@ -227,26 +288,46 @@ export class HttpPlugin extends BasePlugin { const returned = response.end.apply(this, arguments as any); const requestUrl = request.url ? url.parse(request.url) : null; const host = headers.host || 'localhost'; - const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; + const userAgent = (headers['user-agent'] || + headers['User-Agent']) as string; rootSpan - .setAttribute(Attributes.ATTRIBUTE_HTTP_HOST, host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')) + .setAttribute( + Attributes.ATTRIBUTE_HTTP_HOST, + host.replace(/^(.*)(\:[0-9]{1,5})/, '$1') + ) .setAttribute(Attributes.ATTRIBUTE_HTTP_METHOD, method); if (requestUrl) { rootSpan - .setAttribute(Attributes.ATTRIBUTE_HTTP_PATH, requestUrl.pathname || '/') - .setAttribute(Attributes.ATTRIBUTE_HTTP_ROUTE, requestUrl.path || '/'); + .setAttribute( + Attributes.ATTRIBUTE_HTTP_PATH, + requestUrl.pathname || '/' + ) + .setAttribute( + Attributes.ATTRIBUTE_HTTP_ROUTE, + requestUrl.path || '/' + ); } if (userAgent) { - rootSpan.setAttribute(Attributes.ATTRIBUTE_HTTP_USER_AGENT, userAgent); + rootSpan.setAttribute( + Attributes.ATTRIBUTE_HTTP_USER_AGENT, + userAgent + ); } rootSpan - .setAttribute(Attributes.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString()) + .setAttribute( + Attributes.ATTRIBUTE_HTTP_STATUS_CODE, + response.statusCode.toString() + ) .setStatus(Utils.parseResponseStatus(response.statusCode)); if (plugin.options.applyCustomAttributesOnSpan) { - plugin.options.applyCustomAttributesOnSpan(rootSpan, request, response); + plugin.options.applyCustomAttributesOnSpan( + rootSpan, + request, + response + ); } rootSpan.end(); @@ -257,7 +338,9 @@ export class HttpPlugin extends BasePlugin { }; } - private outgoingRequestFunction(original: Func): Func { + private outgoingRequestFunction( + original: Func + ): Func { const plugin = this; return function outgoingRequest( this: {}, @@ -286,7 +369,13 @@ export class HttpPlugin extends BasePlugin { } catch (ignore) {} } const request: ClientRequest = original.apply(this, [options, callback]); - if (Utils.isIgnored(origin + pathname, request, plugin.options.ignoreOutgoingUrls)) { + if ( + Utils.isIgnored( + origin + pathname, + request, + plugin.options.ignoreOutgoingUrls + ) + ) { return request; } plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); @@ -296,7 +385,7 @@ export class HttpPlugin extends BasePlugin { const method = options.method ? options.method.toUpperCase() : 'GET'; const operationName = `${method} ${pathname}`; const spanOptions = { - kind: SpanKind.CLIENT + kind: SpanKind.CLIENT, }; const currentSpan = plugin._tracer.getCurrentSpan(); // Checks if this outgoing request is part of an operation by checking @@ -306,12 +395,15 @@ export class HttpPlugin extends BasePlugin { if (!currentSpan) { plugin._logger.debug('outgoingRequest starting a root span'); const rootSpan = plugin._tracer.startSpan(operationName, spanOptions); - return plugin._tracer.withSpan(rootSpan, plugin.getMakeRequestTraceFunction(request, options)); + return plugin._tracer.withSpan( + rootSpan, + plugin.getMakeRequestTraceFunction(request, options) + ); } else { plugin._logger.debug('outgoingRequest starting a child span'); const span = plugin._tracer.startSpan(operationName, { kind: spanOptions.kind, - parent: currentSpan + parent: currentSpan, }); return plugin.getMakeRequestTraceFunction(request, options)(span); } diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index 2db0be2c52..1d13448068 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -15,10 +15,19 @@ */ import { Span } from '@opentelemetry/types'; -import { ClientRequest, IncomingMessage, ServerResponse, request, get } from 'http'; +import { + ClientRequest, + IncomingMessage, + ServerResponse, + request, + get, +} from 'http'; import * as http from 'http'; -export type IgnoreMatcher = string | RegExp | ((url: string, request: T) => boolean); +export type IgnoreMatcher = + | string + | RegExp + | ((url: string, request: T) => boolean); export type HttpCallback = (res: IncomingMessage) => void; export type RequestFunction = typeof request; export type GetFunction = typeof get; @@ -31,7 +40,11 @@ export type ResponseEndArgs = | [unknown, string, ((() => void) | undefined)?]; export interface HttpCustomAttributeFunction { - (span: Span, request: ClientRequest | IncomingMessage, response: IncomingMessage | ServerResponse): void; + ( + span: Span, + request: ClientRequest | IncomingMessage, + response: IncomingMessage | ServerResponse + ): void; } export interface HttpPluginConfig { diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 179a1656ee..1c5ba4ad2a 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -1,7 +1,7 @@ import { Status, CanonicalCode, Span } from '@opentelemetry/types'; import { RequestOptions, IncomingMessage, ClientRequest } from 'http'; import { IgnoreMatcher } from './types'; -import { Attributes } from './enums/Attributes'; +import { Attributes } from './enums/attributes'; import * as url from 'url'; /** @@ -45,7 +45,10 @@ export class Utils { * @param options Options for http.request. */ static hasExpectHeader(options: RequestOptions | url.URL): boolean { - return !!((options as RequestOptions).headers && (options as RequestOptions).headers!.Expect); + return !!( + (options as RequestOptions).headers && + (options as RequestOptions).headers!.Expect + ); } /** @@ -54,7 +57,11 @@ export class Utils { * @param obj obj to inspect * @param pattern Match pattern */ - static isSatisfyPattern(constant: string, obj: T, pattern: IgnoreMatcher): boolean { + static isSatisfyPattern( + constant: string, + obj: T, + pattern: IgnoreMatcher + ): boolean { if (typeof pattern === 'string') { return pattern === constant; } else if (pattern instanceof RegExp) { @@ -72,7 +79,11 @@ export class Utils { * @param obj obj to inspect * @param list List of ignore patterns */ - static isIgnored(constant: string, obj: T, list?: Array>): boolean { + static isIgnored( + constant: string, + obj: T, + list?: Array> + ): boolean { if (!list) { // No ignored urls - trace everything return false; @@ -97,12 +108,14 @@ export class Utils { span.setAttributes({ [Attributes.ATTRIBUTE_ERROR]: true, [Attributes.ATTRIBUTE_HTTP_ERROR_NAME]: error.name, - [Attributes.ATTRIBUTE_HTTP_ERROR_MESSAGE]: error.message + [Attributes.ATTRIBUTE_HTTP_ERROR_MESSAGE]: error.message, }); let status: Status; if ((obj as IncomingMessage).statusCode) { - status = Utils.parseResponseStatus((obj as IncomingMessage).statusCode!); + status = Utils.parseResponseStatus( + (obj as IncomingMessage).statusCode! + ); } else { status = { code: CanonicalCode.UNKNOWN }; } diff --git a/packages/opentelemetry-plugin-http/test/http-disable.test.ts b/packages/opentelemetry-plugin-http/test/http-disable.test.ts index a3dd741fe6..fcd7d8912d 100644 --- a/packages/opentelemetry-plugin-http/test/http-disable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-disable.test.ts @@ -42,7 +42,7 @@ const httpRequest = { }); }); }); - } + }, }; class DummyPropagation implements HttpTextFormat { @@ -50,7 +50,11 @@ class DummyPropagation implements HttpTextFormat { return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; } - inject(spanContext: SpanContext, format: string, headers: http.IncomingHttpHeaders): void { + inject( + spanContext: SpanContext, + format: string, + headers: http.IncomingHttpHeaders + ): void { headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; } @@ -67,7 +71,7 @@ describe('HttpPlugin', () => { const tracer = new NodeTracer({ scopeManager, logger, - httpTextFormat + httpTextFormat, }); before(() => { plugin.enable(http, tracer); @@ -104,9 +108,15 @@ describe('HttpPlugin', () => { const options = { host: 'localhost', path: testPath, port: serverPort }; await httpRequest.get(options).then(result => { - assert.strictEqual((tracer.startSpan as sinon.SinonSpy).called, false); + assert.strictEqual( + (tracer.startSpan as sinon.SinonSpy).called, + false + ); assert.strictEqual((tracer.withSpan as sinon.SinonSpy).called, false); - assert.strictEqual((tracer.recordSpanData as sinon.SinonSpy).called, false); + assert.strictEqual( + (tracer.recordSpanData as sinon.SinonSpy).called, + false + ); }); }); }); diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/http-enable.test.ts index d2fabc2c03..fa001b855c 100644 --- a/packages/opentelemetry-plugin-http/test/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-enable.test.ts @@ -30,7 +30,11 @@ class DummyPropagation implements HttpTextFormat { return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; } - inject(spanContext: SpanContext, format: string, headers: http.IncomingHttpHeaders): void { + inject( + spanContext: SpanContext, + format: string, + headers: http.IncomingHttpHeaders + ): void { headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; } @@ -47,7 +51,7 @@ describe('HttpPlugin', () => { const tracer = new NodeTracer({ scopeManager, logger, - httpTextFormat + httpTextFormat, }); before(() => { plugin.enable( diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts index 4c9c1cb68a..7b5f9b6a67 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -24,7 +24,9 @@ import { Utils } from '../src/utils'; describe('Utils', () => { describe('parseResponseStatus()', () => { it('should return UNKNOWN code by default', () => { - const status = Utils.parseResponseStatus((undefined as unknown) as number); + const status = Utils.parseResponseStatus( + (undefined as unknown) as number + ); assert.deepStrictEqual(status, { code: CanonicalCode.UNKNOWN }); }); @@ -56,7 +58,9 @@ describe('Utils', () => { }); it('should return true on Expect', () => { - const result = Utils.hasExpectHeader({ headers: { Expect: 1 } } as RequestOptions); + const result = Utils.hasExpectHeader({ + headers: { Expect: 1 }, + } as RequestOptions); assert.strictEqual(result, true); }); }); @@ -77,7 +81,11 @@ describe('Utils', () => { it('should throw if type is unknown', () => { try { - Utils.isSatisfyPattern('/TeSt/1', {}, (true as unknown) as IgnoreMatcher<{}>); + Utils.isSatisfyPattern( + '/TeSt/1', + {}, + (true as unknown) as IgnoreMatcher<{}> + ); assert.fail(); } catch (error) { assert.strictEqual(error instanceof TypeError, true); @@ -88,7 +96,8 @@ describe('Utils', () => { const answer1 = Utils.isSatisfyPattern( '/test/home', { headers: {} }, - (url: string, req: { headers: unknown }) => req.headers && url === '/test/home' + (url: string, req: { headers: unknown }) => + req.headers && url === '/test/home' ); assert.strictEqual(answer1, true); const answer2 = Utils.isSatisfyPattern( @@ -112,18 +121,27 @@ describe('Utils', () => { it('should call isSatisfyPattern, n match', () => { const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); - assert.strictEqual((Utils.isSatisfyPattern as sinon.SinonSpy).callCount, 1); + assert.strictEqual( + (Utils.isSatisfyPattern as sinon.SinonSpy).callCount, + 1 + ); }); it('should call isSatisfyPattern, match', () => { const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); - assert.strictEqual((Utils.isSatisfyPattern as sinon.SinonSpy).callCount, 1); + assert.strictEqual( + (Utils.isSatisfyPattern as sinon.SinonSpy).callCount, + 1 + ); }); it('should not call isSatisfyPattern', () => { Utils.isIgnored('/test/1', {}, []); - assert.strictEqual((Utils.isSatisfyPattern as sinon.SinonSpy).callCount, 0); + assert.strictEqual( + (Utils.isSatisfyPattern as sinon.SinonSpy).callCount, + 0 + ); }); it('should return false on empty list', () => { From 42b0ac2065e10df21209322e0c4412e8a923264b Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Tue, 6 Aug 2019 11:08:07 -0400 Subject: [PATCH 06/18] refactor: add mayurkale22 recommendations Signed-off-by: Olivier Albertini --- .../src/enums/attributes.ts | 34 +++++-- .../src/enums/format.ts | 16 +++ .../opentelemetry-plugin-http/src/http.ts | 97 ++++++++----------- .../opentelemetry-plugin-http/src/utils.ts | 53 +++++++++- 4 files changed, 130 insertions(+), 70 deletions(-) diff --git a/packages/opentelemetry-plugin-http/src/enums/attributes.ts b/packages/opentelemetry-plugin-http/src/enums/attributes.ts index 4f2d308032..b3ad192172 100644 --- a/packages/opentelemetry-plugin-http/src/enums/attributes.ts +++ b/packages/opentelemetry-plugin-http/src/enums/attributes.ts @@ -1,17 +1,33 @@ +/** + * Copyright 2019, 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. + */ + /** * Attributes Names according to Opencensus HTTP Specs since there is no specific OpenTelemetry Attributes * https://github.com/open-telemetry/opentelemetry-specification/blob/master/work_in_progress/opencensus/HTTP.md#attributes */ export enum Attributes { - ATTRIBUTE_HTTP_HOST = 'http.host', + HTTP_HOST = 'http.host', // NOT ON OFFICIAL SPEC - ATTRIBUTE_ERROR = 'error', - ATTRIBUTE_HTTP_METHOD = 'http.method', - ATTRIBUTE_HTTP_PATH = 'http.path', - ATTRIBUTE_HTTP_ROUTE = 'http.route', - ATTRIBUTE_HTTP_USER_AGENT = 'http.user_agent', - ATTRIBUTE_HTTP_STATUS_CODE = 'http.status_code', + ERROR = 'error', + HTTP_METHOD = 'http.method', + HTTP_PATH = 'http.path', + HTTP_ROUTE = 'http.route', + HTTP_USER_AGENT = 'http.user_agent', + HTTP_STATUS_CODE = 'http.status_code', // NOT ON OFFICIAL SPEC - ATTRIBUTE_HTTP_ERROR_NAME = 'http.error_name', - ATTRIBUTE_HTTP_ERROR_MESSAGE = 'http.error_message', + HTTP_ERROR_NAME = 'http.error_name', + HTTP_ERROR_MESSAGE = 'http.error_message', } diff --git a/packages/opentelemetry-plugin-http/src/enums/format.ts b/packages/opentelemetry-plugin-http/src/enums/format.ts index 962271af44..8f5e3d5a2e 100644 --- a/packages/opentelemetry-plugin-http/src/enums/format.ts +++ b/packages/opentelemetry-plugin-http/src/enums/format.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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. + */ + // Should we export this to the types package in order to expose all values ? export enum Format { HTTP = 'HttpTraceContext', diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 767520bf7b..177fd3b73d 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -48,7 +48,6 @@ export class HttpPlugin extends BasePlugin { protected readonly _logger!: Logger; protected readonly _tracer!: NodeTracer; - /** Constructs a new HttpPlugin instance. */ constructor(public moduleName: string, public version: string) { super(); // TODO: remove this once a logger will be passed @@ -164,16 +163,12 @@ export class HttpPlugin extends BasePlugin { */ private getMakeRequestTraceFunction( request: ClientRequest, - options: RequestOptions + options: RequestOptions, + span: Span ): Func { - return (span: Span): ClientRequest => { + return (): ClientRequest => { this._logger.debug('makeRequestTrace'); - if (!span) { - this._logger.debug('makeRequestTrace span is null'); - return request; - } - const propagation = this._tracer.getHttpTextFormat(); // If outgoing request headers contain the "Expect" header, the returned // ClientRequest will throw an error if any new headers are added. @@ -200,18 +195,19 @@ export class HttpPlugin extends BasePlugin { const host = options.hostname || options.host || 'localhost'; span.setAttributes({ - ATTRIBUTE_HTTP_HOST: host, - ATTRIBUTE_HTTP_METHOD: method, - ATTRIBUTE_HTTP_PATH: options.path || '/', + [Attributes.HTTP_HOST]: host, + [Attributes.HTTP_METHOD]: method, + [Attributes.HTTP_PATH]: options.path || '/', }); if (userAgent) { - span.setAttribute(Attributes.ATTRIBUTE_HTTP_USER_AGENT, userAgent); + span.setAttribute(Attributes.HTTP_USER_AGENT, userAgent); } + if (response.statusCode) { span .setAttribute( - Attributes.ATTRIBUTE_HTTP_STATUS_CODE, + Attributes.HTTP_STATUS_CODE, response.statusCode.toString() ) .setStatus(Utils.parseResponseStatus(response.statusCode)); @@ -251,6 +247,7 @@ export class HttpPlugin extends BasePlugin { const response = args[1] as ServerResponse; const path = request.url ? url.parse(request.url).pathname || '' : ''; const method = request.method || 'GET'; + plugin._logger.debug('%s plugin incomingRequest', plugin.moduleName); if (Utils.isIgnored(path, request, plugin.options.ignoreIncomingPaths)) { @@ -259,7 +256,6 @@ export class HttpPlugin extends BasePlugin { const propagation = plugin._tracer.getHttpTextFormat(); const headers = request.headers; - const spanOptions: SpanOptions = { kind: SpanKind.SERVER, }; @@ -291,33 +287,25 @@ export class HttpPlugin extends BasePlugin { const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; - rootSpan - .setAttribute( - Attributes.ATTRIBUTE_HTTP_HOST, - host.replace(/^(.*)(\:[0-9]{1,5})/, '$1') - ) - .setAttribute(Attributes.ATTRIBUTE_HTTP_METHOD, method); + rootSpan.setAttributes({ + [Attributes.HTTP_HOST]: host.replace(/^(.*)(\:[0-9]{1,5})/, '$1'), + [Attributes.HTTP_METHOD]: method, + }); if (requestUrl) { - rootSpan - .setAttribute( - Attributes.ATTRIBUTE_HTTP_PATH, - requestUrl.pathname || '/' - ) - .setAttribute( - Attributes.ATTRIBUTE_HTTP_ROUTE, - requestUrl.path || '/' - ); + rootSpan.setAttributes({ + [Attributes.HTTP_PATH]: requestUrl.pathname || '/', + [Attributes.HTTP_ROUTE]: requestUrl.path || '/', + }); } + if (userAgent) { - rootSpan.setAttribute( - Attributes.ATTRIBUTE_HTTP_USER_AGENT, - userAgent - ); + rootSpan.setAttribute(Attributes.HTTP_USER_AGENT, userAgent); } + rootSpan .setAttribute( - Attributes.ATTRIBUTE_HTTP_STATUS_CODE, + Attributes.HTTP_STATUS_CODE, response.statusCode.toString() ) .setStatus(Utils.parseResponseStatus(response.statusCode)); @@ -351,24 +339,14 @@ export class HttpPlugin extends BasePlugin { return original.apply(this, [options, callback]); } - // Makes sure the url is an url object - let pathname; - let origin = ''; - if (typeof options === 'string') { - const parsedUrl = url.parse(options); - options = parsedUrl; - pathname = parsedUrl.pathname || ''; - origin = `${parsedUrl.protocol || 'http:'}//${parsedUrl.host}`; - } else { - try { - pathname = (options as url.URL).pathname; - if (!pathname) { - pathname = options.path ? url.parse(options.path).pathname : ''; - } - origin = `${options.protocol || 'http:'}//${options.host}`; - } catch (ignore) {} - } - const request: ClientRequest = original.apply(this, [options, callback]); + const { origin, pathname, method, optionsParsed } = Utils.getRequestInfo( + options + ); + const request: ClientRequest = original.apply(this, [ + optionsParsed, + callback, + ]); + if ( Utils.isIgnored( origin + pathname, @@ -378,11 +356,10 @@ export class HttpPlugin extends BasePlugin { ) { return request; } + plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); plugin._tracer.wrapEmitter(request); - // some packages return method in lowercase.. - // ensure upperCase for consistency - const method = options.method ? options.method.toUpperCase() : 'GET'; + const operationName = `${method} ${pathname}`; const spanOptions = { kind: SpanKind.CLIENT, @@ -397,7 +374,7 @@ export class HttpPlugin extends BasePlugin { const rootSpan = plugin._tracer.startSpan(operationName, spanOptions); return plugin._tracer.withSpan( rootSpan, - plugin.getMakeRequestTraceFunction(request, options) + plugin.getMakeRequestTraceFunction(request, optionsParsed, rootSpan) ); } else { plugin._logger.debug('outgoingRequest starting a child span'); @@ -405,10 +382,14 @@ export class HttpPlugin extends BasePlugin { kind: spanOptions.kind, parent: currentSpan, }); - return plugin.getMakeRequestTraceFunction(request, options)(span); + return plugin.getMakeRequestTraceFunction( + request, + optionsParsed, + span + )(); } }; } } -export const plugin = new HttpPlugin('http', '8.0.0'); +export const plugin = new HttpPlugin('http', process.versions.node); diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 1c5ba4ad2a..f0b461fcc4 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 { Status, CanonicalCode, Span } from '@opentelemetry/types'; import { RequestOptions, IncomingMessage, ClientRequest } from 'http'; import { IgnoreMatcher } from './types'; @@ -106,9 +122,9 @@ export class Utils { static setSpanOnError(span: Span, obj: IncomingMessage | ClientRequest) { obj.on('error', error => { span.setAttributes({ - [Attributes.ATTRIBUTE_ERROR]: true, - [Attributes.ATTRIBUTE_HTTP_ERROR_NAME]: error.name, - [Attributes.ATTRIBUTE_HTTP_ERROR_MESSAGE]: error.message, + [Attributes.ERROR]: true, + [Attributes.HTTP_ERROR_NAME]: error.name, + [Attributes.HTTP_ERROR_MESSAGE]: error.message, }); let status: Status; @@ -126,4 +142,35 @@ export class Utils { span.end(); }); } + + /** + * Makes sure options is an url object + * return an object with default value and parsed options + * @param options for the request + */ + static getRequestInfo(options: RequestOptions | string) { + let pathname = '/'; + let origin = ''; + let optionsParsed: url.URL | url.UrlWithStringQuery | RequestOptions; + if (typeof options === 'string') { + optionsParsed = url.parse(options); + pathname = (optionsParsed as url.UrlWithStringQuery).pathname || '/'; + origin = `${optionsParsed.protocol || 'http:'}//${optionsParsed.host}`; + } else { + optionsParsed = options; + try { + pathname = (options as url.URL).pathname; + if (!pathname && options.path) { + pathname = url.parse(options.path).pathname || '/'; + } + origin = `${options.protocol || 'http:'}//${options.host}`; + } catch (ignore) {} + } + // some packages return method in lowercase.. + // ensure upperCase for consistency + let method = (optionsParsed as RequestOptions).method; + method = method ? method.toUpperCase() : 'GET'; + + return { origin, pathname, method, optionsParsed }; + } } From f92a68cdea93ad93e8b23c0448e7c3bdc45d8da5 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Thu, 8 Aug 2019 16:26:49 -0400 Subject: [PATCH 07/18] fix: integrate bg451 recommendations test: increase coverage fix: attributes requirements from the spec. Signed-off-by: Olivier Albertini --- .../src/BasicTracer.ts | 9 + .../src/trace/NoopTracer.ts | 3 + .../src/trace/TracerDelegate.ts | 8 + .../src/NodeTracer.ts | 13 - .../opentelemetry-plugin-http/package.json | 3 +- .../src/enums/attributes.ts | 7 +- .../opentelemetry-plugin-http/src/http.ts | 121 +++--- .../src/models/spanFactory.ts | 17 + .../opentelemetry-plugin-http/src/types.ts | 4 + .../opentelemetry-plugin-http/src/utils.ts | 46 ++- .../test/http-enable.test.ts | 358 +++++++++++++++--- .../test/utils.test.ts | 51 ++- .../test/utils/DummyPropagation.ts | 23 ++ .../test/utils/ProxyTracer.ts | 39 ++ .../test/utils/SpanAudit.ts | 23 ++ .../test/utils/SpanAuditProcessor.ts | 40 ++ .../test/utils/assertSpan.ts | 72 ++++ .../test/utils/httpRequest.ts | 48 +++ .../opentelemetry-types/src/trace/tracer.ts | 12 + 19 files changed, 766 insertions(+), 131 deletions(-) create mode 100644 packages/opentelemetry-plugin-http/src/models/spanFactory.ts create mode 100644 packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts create mode 100644 packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts create mode 100644 packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts create mode 100644 packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts create mode 100644 packages/opentelemetry-plugin-http/test/utils/assertSpan.ts create mode 100644 packages/opentelemetry-plugin-http/test/utils/httpRequest.ts diff --git a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts index afa09ada98..85462bf480 100644 --- a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts +++ b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts @@ -125,6 +125,15 @@ export class BasicTracer implements types.Tracer { // Set given span to context. return this._scopeManager.with(span, fn); } + /** + * Binds the trace context to the given event emitter + */ + wrapEmitter(emitter: NodeJS.EventEmitter): void { + if (!this._scopeManager.active()) { + return; + } + this._scopeManager.bind(emitter); + } /** * Bind a span (or the current one) to the target's scope diff --git a/packages/opentelemetry-core/src/trace/NoopTracer.ts b/packages/opentelemetry-core/src/trace/NoopTracer.ts index d10c5f6fc3..3f9b36306d 100644 --- a/packages/opentelemetry-core/src/trace/NoopTracer.ts +++ b/packages/opentelemetry-core/src/trace/NoopTracer.ts @@ -61,4 +61,7 @@ export class NoopTracer implements Tracer { getHttpTextFormat(): HttpTextFormat { return NOOP_HTTP_TEXT_FORMAT; } + + // By default does nothing + wrapEmitter(emitter: unknown): void {} } diff --git a/packages/opentelemetry-core/src/trace/TracerDelegate.ts b/packages/opentelemetry-core/src/trace/TracerDelegate.ts index 0fe563a0ab..607e7d6888 100644 --- a/packages/opentelemetry-core/src/trace/TracerDelegate.ts +++ b/packages/opentelemetry-core/src/trace/TracerDelegate.ts @@ -82,6 +82,14 @@ export class TracerDelegate implements types.Tracer { ); } + wrapEmitter(emitter: unknown): void { + this._currentTracer.wrapEmitter.apply( + this._currentTracer, + // tslint:disable-next-line:no-any + arguments as any + ); + } + recordSpanData(span: types.Span): void { return this._currentTracer.recordSpanData.apply( this._currentTracer, diff --git a/packages/opentelemetry-node-tracer/src/NodeTracer.ts b/packages/opentelemetry-node-tracer/src/NodeTracer.ts index ff822d0067..ea138470e5 100644 --- a/packages/opentelemetry-node-tracer/src/NodeTracer.ts +++ b/packages/opentelemetry-node-tracer/src/NodeTracer.ts @@ -31,17 +31,4 @@ export class NodeTracer extends BasicTracer { // @todo: Integrate Plugin Loader (pull/126). } - /** - * Binds the trace context to the given event emitter. - * This is necessary in order to create child spans correctly in event - * handlers. - * @param emitter An event emitter whose handlers should have - * the trace context binded to them. - */ - wrapEmitter(emitter: NodeJS.EventEmitter): void { - if (!this._scopeManager.active()) { - return; - } - this._scopeManager.bind(emitter); - } } diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index d5a04d7980..474dd1372b 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -6,7 +6,7 @@ "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", "scripts": { - "test": "nyc ts-mocha -p tsconfig.json test/**/*.ts", + "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", "check": "gts check", @@ -45,6 +45,7 @@ "@types/shimmer": "^1.0.1", "@types/sinon":"^7.0.13", "@opentelemetry/scope-async-hooks": "^0.0.1", + "@opentelemetry/scope-base": "^0.0.1", "codecov": "^3.5.0", "gts": "^1.0.0", "mocha": "^6.2.0", diff --git a/packages/opentelemetry-plugin-http/src/enums/attributes.ts b/packages/opentelemetry-plugin-http/src/enums/attributes.ts index b3ad192172..024477a6c1 100644 --- a/packages/opentelemetry-plugin-http/src/enums/attributes.ts +++ b/packages/opentelemetry-plugin-http/src/enums/attributes.ts @@ -19,15 +19,18 @@ * https://github.com/open-telemetry/opentelemetry-specification/blob/master/work_in_progress/opencensus/HTTP.md#attributes */ export enum Attributes { - HTTP_HOST = 'http.host', + HTTP_HOSTNAME = 'http.hostname', // NOT ON OFFICIAL SPEC ERROR = 'error', + COMPONENT = 'component', HTTP_METHOD = 'http.method', HTTP_PATH = 'http.path', HTTP_ROUTE = 'http.route', - HTTP_USER_AGENT = 'http.user_agent', + HTTP_URL = 'http.url', HTTP_STATUS_CODE = 'http.status_code', + HTTP_STATUS_TEXT = 'http.status_text', // NOT ON OFFICIAL SPEC HTTP_ERROR_NAME = 'http.error_name', HTTP_ERROR_MESSAGE = 'http.error_message', + HTTP_USER_AGENT = 'http.user_agent', } diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 177fd3b73d..93dc707300 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -15,8 +15,13 @@ */ import { BasePlugin, NoopLogger, isValid } from '@opentelemetry/core'; -import { Span, SpanKind, SpanOptions, Logger } from '@opentelemetry/types'; -import { NodeTracer } from '@opentelemetry/node-tracer'; +import { + Span, + SpanKind, + SpanOptions, + Logger, + Tracer, +} from '@opentelemetry/types'; import { ClientRequest, IncomingMessage, @@ -33,20 +38,27 @@ import { Func, HttpCallback, ResponseEndArgs, + ParsedRequestOptions, } from './types'; import { Format } from './enums/format'; import { Attributes } from './enums/attributes'; import { Utils } from './utils'; +import { SpanFactory } from './models/spanFactory'; /** * Http instrumentation plugin for Opentelemetry */ export class HttpPlugin extends BasePlugin { + static readonly component = 'http'; options!: HttpPluginConfig; // TODO: BasePlugin should pass the logger or when we enable the plugin - protected readonly _logger!: Logger; - protected readonly _tracer!: NodeTracer; + protected _logger!: Logger; + protected readonly _tracer!: Tracer; + /** + * Used to create span with default attributes + */ + protected _spanFactory!: SpanFactory; constructor(public moduleName: string, public version: string) { super(); @@ -56,6 +68,7 @@ export class HttpPlugin extends BasePlugin { /** Patches HTTP incoming and outcoming request functions. */ protected patch() { + this._spanFactory = new SpanFactory(this._tracer, HttpPlugin.component); this._logger.debug( 'applying patch to %s@%s', this.moduleName, @@ -117,7 +130,7 @@ export class HttpPlugin extends BasePlugin { * Creates spans for incoming requests, restoring spans' context if applied. */ protected _getPatchIncomingRequestFunction() { - return (original: (event: string) => boolean) => { + return (original: (event: string, ...args: unknown[]) => boolean) => { return this.incomingRequestFunction(original); }; } @@ -163,41 +176,40 @@ export class HttpPlugin extends BasePlugin { */ private getMakeRequestTraceFunction( request: ClientRequest, - options: RequestOptions, + options: ParsedRequestOptions, span: Span ): Func { return (): ClientRequest => { this._logger.debug('makeRequestTrace'); const propagation = this._tracer.getHttpTextFormat(); - // If outgoing request headers contain the "Expect" header, the returned - // ClientRequest will throw an error if any new headers are added. - // So we need to clone the options object to be able to inject new - // header. - if (Utils.hasExpectHeader(options)) { - options = Object.assign({}, options); - options.headers = Object.assign({}, options.headers); - } - propagation.inject(span.context(), Format.HTTP, options.headers); + const opts = Utils.getIncomingOptions(options); + + propagation.inject(span.context(), Format.HTTP, opts.headers); request.on('response', (response: IncomingMessage) => { this._tracer.wrapEmitter(response); this._logger.debug('outgoingRequest on response()'); response.on('end', () => { this._logger.debug('outgoingRequest on end()'); + + // TODO: create utils methods const method = response.method ? response.method.toUpperCase() : 'GET'; - const headers = options.headers; + const headers = opts.headers; const userAgent = headers ? headers['user-agent'] || headers['User-Agent'] : null; - const host = options.hostname || options.host || 'localhost'; + const host = opts.hostname || opts.host || 'localhost'; span.setAttributes({ - [Attributes.HTTP_HOST]: host, + [Attributes.HTTP_URL]: `${opts.protocol}//${opts.hostname}${ + (opts as url.UrlWithParsedQuery).pathname + }`, + [Attributes.HTTP_HOSTNAME]: host, [Attributes.HTTP_METHOD]: method, - [Attributes.HTTP_PATH]: options.path || '/', + [Attributes.HTTP_PATH]: opts.path || '/', }); if (userAgent) { @@ -206,10 +218,10 @@ export class HttpPlugin extends BasePlugin { if (response.statusCode) { span - .setAttribute( - Attributes.HTTP_STATUS_CODE, - response.statusCode.toString() - ) + .setAttributes({ + [Attributes.HTTP_STATUS_CODE]: response.statusCode, + [Attributes.HTTP_STATUS_TEXT]: response.statusMessage, + }) .setStatus(Utils.parseResponseStatus(response.statusCode)); } @@ -245,7 +257,7 @@ export class HttpPlugin extends BasePlugin { const request = args[0] as IncomingMessage; const response = args[1] as ServerResponse; - const path = request.url ? url.parse(request.url).pathname || '' : ''; + const path = request.url ? url.parse(request.url).pathname || '/' : '/'; const method = request.method || 'GET'; plugin._logger.debug('%s plugin incomingRequest', plugin.moduleName); @@ -265,8 +277,12 @@ export class HttpPlugin extends BasePlugin { spanOptions.parent = spanContext; } - const rootSpan = plugin._tracer.startSpan(path, spanOptions); - return plugin._tracer.withSpan(rootSpan, () => { + const span = plugin._spanFactory.createInstance( + `${method} ${path}`, + spanOptions + ); + + return plugin._tracer.withSpan(span, () => { plugin._tracer.wrapEmitter(request); plugin._tracer.wrapEmitter(response); @@ -283,42 +299,44 @@ export class HttpPlugin extends BasePlugin { // tslint:disable-next-line:no-any const returned = response.end.apply(this, arguments as any); const requestUrl = request.url ? url.parse(request.url) : null; - const host = headers.host || 'localhost'; + const hostname = headers.host + ? headers.host.replace(/^(.*)(\:[0-9]{1,5})/, '$1') + : 'localhost'; const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; - rootSpan.setAttributes({ - [Attributes.HTTP_HOST]: host.replace(/^(.*)(\:[0-9]{1,5})/, '$1'), + span.setAttributes({ + [Attributes.HTTP_URL]: Utils.getUrlFromIncomingRequest( + requestUrl, + headers + ), + [Attributes.HTTP_HOSTNAME]: hostname, [Attributes.HTTP_METHOD]: method, }); if (requestUrl) { - rootSpan.setAttributes({ + span.setAttributes({ [Attributes.HTTP_PATH]: requestUrl.pathname || '/', [Attributes.HTTP_ROUTE]: requestUrl.path || '/', }); } if (userAgent) { - rootSpan.setAttribute(Attributes.HTTP_USER_AGENT, userAgent); + span.setAttribute(Attributes.HTTP_USER_AGENT, userAgent); } - rootSpan - .setAttribute( - Attributes.HTTP_STATUS_CODE, - response.statusCode.toString() - ) + span + .setAttributes({ + [Attributes.HTTP_STATUS_CODE]: response.statusCode, + [Attributes.HTTP_STATUS_TEXT]: response.statusMessage, + }) .setStatus(Utils.parseResponseStatus(response.statusCode)); if (plugin.options.applyCustomAttributesOnSpan) { - plugin.options.applyCustomAttributesOnSpan( - rootSpan, - request, - response - ); + plugin.options.applyCustomAttributesOnSpan(span, request, response); } - rootSpan.end(); + span.end(); return returned; }; return original.apply(this, [event, ...args]); @@ -366,19 +384,22 @@ export class HttpPlugin extends BasePlugin { }; const currentSpan = plugin._tracer.getCurrentSpan(); // Checks if this outgoing request is part of an operation by checking - // if there is a current root span, if so, we create a child span. In - // case there is no root span, this means that the outgoing request is - // the first operation, therefore we create a root span. + // if there is a current span, if so, we create a child span. In + // case there is no active span, this means that the outgoing request is + // the first operation, therefore we create a span and call withSpan method. if (!currentSpan) { - plugin._logger.debug('outgoingRequest starting a root span'); - const rootSpan = plugin._tracer.startSpan(operationName, spanOptions); + plugin._logger.debug('outgoingRequest starting a span without context'); + const span = plugin._spanFactory.createInstance( + operationName, + spanOptions + ); return plugin._tracer.withSpan( - rootSpan, - plugin.getMakeRequestTraceFunction(request, optionsParsed, rootSpan) + span, + plugin.getMakeRequestTraceFunction(request, optionsParsed, span) ); } else { plugin._logger.debug('outgoingRequest starting a child span'); - const span = plugin._tracer.startSpan(operationName, { + const span = plugin._spanFactory.createInstance(operationName, { kind: spanOptions.kind, parent: currentSpan, }); diff --git a/packages/opentelemetry-plugin-http/src/models/spanFactory.ts b/packages/opentelemetry-plugin-http/src/models/spanFactory.ts new file mode 100644 index 0000000000..87a41c9b0c --- /dev/null +++ b/packages/opentelemetry-plugin-http/src/models/spanFactory.ts @@ -0,0 +1,17 @@ +import { Span, SpanOptions, Tracer } from '@opentelemetry/types'; +import { Attributes } from '../enums/attributes'; +/** + * Create span with default attributes + */ +export class SpanFactory { + private readonly _tracer: Tracer; + constructor(tracer: Tracer, public component: string) { + this._tracer = tracer; + } + + createInstance(name: string, options?: SpanOptions): Span { + return this._tracer + .startSpan(name, options) + .setAttribute(Attributes.COMPONENT, this.component); + } +} diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index 1d13448068..a9c43cc294 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -15,6 +15,7 @@ */ import { Span } from '@opentelemetry/types'; +import * as url from 'url'; import { ClientRequest, IncomingMessage, @@ -31,6 +32,9 @@ export type IgnoreMatcher = export type HttpCallback = (res: IncomingMessage) => void; export type RequestFunction = typeof request; export type GetFunction = typeof get; +export type ParsedRequestOptions = + | http.RequestOptions & Partial + | http.RequestOptions; export type Http = typeof http; /* tslint:disable-next-line:no-any */ export type Func = (...args: any[]) => T; diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index f0b461fcc4..22a1a60137 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -15,8 +15,13 @@ */ import { Status, CanonicalCode, Span } from '@opentelemetry/types'; -import { RequestOptions, IncomingMessage, ClientRequest } from 'http'; -import { IgnoreMatcher } from './types'; +import { + RequestOptions, + IncomingMessage, + ClientRequest, + IncomingHttpHeaders, +} from 'http'; +import { IgnoreMatcher, ParsedRequestOptions } from './types'; import { Attributes } from './enums/attributes'; import * as url from 'url'; @@ -24,6 +29,22 @@ import * as url from 'url'; * Utility class */ export class Utils { + /** + * return an absolute url + */ + static getUrlFromIncomingRequest( + requestUrl: url.UrlWithStringQuery | null, + headers: IncomingHttpHeaders + ): string { + if (!requestUrl) { + return `http://${headers.host || 'localhost'}/`; + } + + return requestUrl.href && requestUrl.href.startsWith('http') + ? `${requestUrl.protocol}//${requestUrl.hostname}${requestUrl.pathname}` + : `${requestUrl.protocol || 'http:'}//${headers.host || + 'localhost'}${requestUrl.pathname || '/'}`; + } /** * Parse status code from HTTP response. */ @@ -73,7 +94,7 @@ export class Utils { * @param obj obj to inspect * @param pattern Match pattern */ - static isSatisfyPattern( + static satisfiesPattern( constant: string, obj: T, pattern: IgnoreMatcher @@ -106,7 +127,7 @@ export class Utils { } for (const pattern of list) { - if (Utils.isSatisfyPattern(constant, obj, pattern)) { + if (Utils.satisfiesPattern(constant, obj, pattern)) { return true; } } @@ -173,4 +194,21 @@ export class Utils { return { origin, pathname, method, optionsParsed }; } + + static getIncomingOptions(options: ParsedRequestOptions) { + // If outgoing request headers contain the "Expect" header, the returned + // ClientRequest will throw an error if any new headers are added. + // So we need to clone the options object to be able to inject new + // header. + if (Utils.hasExpectHeader(options)) { + const safeOptions = Object.assign({}, options); + safeOptions.headers = Object.assign({}, options.headers); + return safeOptions; + } + + if (!options.headers) { + options.headers = {}; + } + return options; + } } diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/http-enable.test.ts index fa001b855c..78b5a2dd2e 100644 --- a/packages/opentelemetry-plugin-http/test/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-enable.test.ts @@ -14,84 +14,344 @@ * limitations under the License. */ +import { NoopLogger } from '@opentelemetry/core'; +import { NodeTracer } from '@opentelemetry/node-tracer'; +import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; +import { SpanKind, Span } from '@opentelemetry/types'; import * as assert from 'assert'; import * as http from 'http'; import * as nock from 'nock'; - import { HttpPlugin, plugin } from '../src/http'; -import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; -import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; -import { NodeTracer } from '@opentelemetry/node-tracer'; -import { NoopLogger } from '@opentelemetry/core'; -import { AddressInfo } from 'net'; - -class DummyPropagation implements HttpTextFormat { - extract(format: string, carrier: unknown): SpanContext { - return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; - } - - inject( - spanContext: SpanContext, - format: string, - headers: http.IncomingHttpHeaders - ): void { - headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; - headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; - } +import { assertSpan } from './utils/assertSpan'; +import { DummyPropagation } from './utils/DummyPropagation'; +import { httpRequest } from './utils/httpRequest'; +import { ProxyTracer } from './utils/ProxyTracer'; +import { SpanAuditProcessor } from './utils/SpanAuditProcessor'; +import * as url from 'url'; + +let server: http.Server; +const serverPort = 12345; +const protocol = 'http'; +const hostname = 'localhost'; +const pathname = '/test'; +const audit = new SpanAuditProcessor(); + +function doNock( + hostname: string, + path: string, + httpCode: number, + respBody: string, + times?: number +) { + const i = times || 1; + nock(`${protocol}://${hostname}`) + .get(path) + .times(i) + .reply(httpCode, respBody); } +export const customAttributeFunction = ( + span: Span, + request: http.ClientRequest | http.IncomingMessage, + response: http.IncomingMessage | http.ServerResponse +): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + describe('HttpPlugin', () => { - let server: http.Server; - let serverPort = 0; + it('should return a plugin', () => { + assert.ok(plugin instanceof HttpPlugin); + }); + + it('should match version', () => { + assert.strictEqual(process.versions.node, plugin.version); + }); + + it('moduleName should be http', () => { + assert.strictEqual('http', plugin.moduleName); + }); describe('enable()', () => { const scopeManager = new AsyncHooksScopeManager(); const httpTextFormat = new DummyPropagation(); const logger = new NoopLogger(); - const tracer = new NodeTracer({ + const realTracer = new NodeTracer({ scopeManager, logger, httpTextFormat, }); + const tracer = new ProxyTracer(realTracer, audit); + beforeEach(() => { + audit.reset(); + }); + before(() => { - plugin.enable( - http, - tracer - // { - // ignoreIncomingPaths: [ - // '/ignored/string', - // /^\/ignored\/regexp/, - // (url: string) => url === '/ignored/function', - // ], - // ignoreOutgoingUrls: [ - // `${urlHost}/ignored/string`, - // /^http:\/\/fake\.service\.io\/ignored\/regexp$/, - // (url: string) => url === `${urlHost}/ignored/function`, - // ], - // applyCustomAttributesOnSpan: customAttributeFunction, - // }, - ); + plugin.enable(http, tracer); + const ignoreConfig = [ + `http://${hostname}:${serverPort}/ignored/string`, + /\/ignored\/regexp$/i, + (url: string) => url.endsWith(`/ignored/function`), + ]; + plugin.options = { + ignoreIncomingPaths: ignoreConfig, + ignoreOutgoingUrls: ignoreConfig, + applyCustomAttributesOnSpan: customAttributeFunction, + }; + server = http.createServer((request, response) => { response.end('Test Server Response'); }); server.listen(serverPort); - server.once('listening', () => { - serverPort = (server.address() as AddressInfo).port; + }); + + after(() => { + server.close(); + }); + + it('should generate valid span (client side and server side)', done => { + httpRequest + .get(`http://${hostname}:${serverPort}${pathname}`) + .then(result => { + const spans = audit.processSpans(); + const outgoingSpan = spans[0]; + const incomingSpan = spans[1]; + + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: result.method!, + pathname, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + + assert.strictEqual(spans.length, 2); + assertSpan(incomingSpan, SpanKind.SERVER, validations); + assertSpan(outgoingSpan, SpanKind.CLIENT, validations); + done(); + }); + }); + + const httpErrorCodes = [400, 401, 403, 404, 429, 501, 503, 504, 500]; + + for (let i = 0; i < httpErrorCodes.length; i++) { + it(`should test span for GET requests with http error ${httpErrorCodes[i]}`, async () => { + const testPath = '/outgoing/rootSpan/1'; + doNock( + hostname, + testPath, + httpErrorCodes[i], + httpErrorCodes[i].toString() + ); + const isReset = audit.processSpans().length === 0; + assert.ok(isReset); + await httpRequest + .get(`${protocol}://${hostname}${testPath}`) + .then(result => { + const spans = audit.processSpans(); + assert.strictEqual(result.data, httpErrorCodes[i].toString()); + assert.strictEqual(spans.length, 1); + + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + + assertSpan(spans[0], SpanKind.CLIENT, validations); + }); + }); + } + + it('should create a child span for GET requests', done => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock(hostname, testPath, 200, 'Ok'); + const name = 'TestRootSpan'; + const span = tracer.startSpan(name); + tracer.withSpan(span, () => { + httpRequest.get(`${protocol}://${hostname}${testPath}`).then(result => { + const spans = audit.processSpans(); + assert.ok(spans[0].name.indexOf('TestRootSpan') >= 0); + assert.strictEqual(spans.length, 2); + assert.ok(spans[1].name.indexOf(testPath) >= 0); + assert.strictEqual( + spans[1].spanContext.traceId, + spans[0].spanContext.traceId + ); + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + assertSpan(spans[1], SpanKind.CLIENT, validations); + assert.notStrictEqual( + spans[1].spanContext.spanId, + spans[0].spanContext.spanId + ); + done(); + }); }); - nock.disableNetConnect(); }); - beforeEach(() => { - nock.cleanAll(); + for (let i = 0; i < httpErrorCodes.length; i++) { + it(`should test a child spans for GET requests with http error ${httpErrorCodes[i]}`, done => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock( + hostname, + testPath, + httpErrorCodes[i], + httpErrorCodes[i].toString() + ); + const name = 'TestRootSpan'; + const span = tracer.startSpan(name); + tracer.withSpan(span, () => { + httpRequest + .get(`${protocol}://${hostname}${testPath}`) + .then(result => { + const spans = audit.processSpans(); + assert.ok(spans[0].name.indexOf('TestRootSpan') >= 0); + assert.strictEqual(spans.length, 2); + assert.ok(spans[1].name.indexOf(testPath) >= 0); + assert.strictEqual( + spans[1].spanContext.traceId, + spans[0].spanContext.traceId + ); + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + assertSpan(spans[1], SpanKind.CLIENT, validations); + assert.notStrictEqual( + spans[1].spanContext.spanId, + spans[0].spanContext.spanId + ); + done(); + }); + }); + }); + } + // TODO: uncomment once https://github.com/open-telemetry/opentelemetry-js/pull/146 is merged + // it('should create multiple child spans for GET requests', (done) => { + // const testPath = '/outgoing/rootSpan/childs'; + // const num = 5; + // doNock(hostname, testPath, 200, 'Ok', num); + // const name = 'TestRootSpan'; + // const span = tracer.startSpan(name); + // const auditedSpan = audit.processSpans()[0]; + // assert.ok(auditedSpan.name.indexOf('TestRootSpan') >= 0); + // tracer.withSpan(span, async () => { + // for (let i = 0; i < num; i++) { + // await httpRequest.get(`${ protocol }://${ hostname }${ testPath }`).then(result => { + // const spans = audit.processSpans(); + // const startChildIndex = i + 1; + // assert.strictEqual(spans.length, startChildIndex + 1); + // assert.ok(spans[startChildIndex].name.indexOf(testPath) >= 0); + // assert.strictEqual(auditedSpan.spanContext.traceId, spans[startChildIndex].spanContext.traceId); + // }); + // } + // const spans = audit.processSpans(); + // // 5 child spans ended + 1 span (root) + // assert.strictEqual(spans.length, 6); + // span.end(); + // done(); + // }); + // }); + + for (const ignored of ['string', 'function', 'regexp']) { + it(`should not trace ignored requests with type ${ignored}`, async () => { + const testPath = `/ignored/${ignored}`; + doNock(hostname, testPath, 200, 'Ok'); + + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 0); + await httpRequest.get(`${protocol}://${hostname}${testPath}`); + assert.strictEqual(spans.length, 0); + }); + } + + it('should create a rootSpan for GET requests and add propagation headers', async () => { + nock.enableNetConnect(); + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 0); + await httpRequest.get(`http://google.fr/?query=test`).then(result => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + assertSpan(span, SpanKind.CLIENT, validations); + }); + nock.disableNetConnect(); }); - after(() => { - server.close(); + it('custom attributes should show up on client spans', async () => { + nock.enableNetConnect(); + + await httpRequest.get(`http://google.fr/`).then(result => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); + assertSpan(span, SpanKind.CLIENT, validations); + }); + nock.disableNetConnect(); }); - it('should return a plugin', () => { - assert.ok(plugin instanceof HttpPlugin); + it('should create a span for GET requests and add propagation headers with Expect headers', async () => { + nock.enableNetConnect(); + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 0); + const options = Object.assign( + { headers: { Expect: '100-continue' } }, + url.parse('http://google.fr/') + ); + await httpRequest.get(options).then(result => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + assertSpan(span, SpanKind.CLIENT, validations); + }); + nock.disableNetConnect(); }); }); }); diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts index 7b5f9b6a67..f4f32cadc4 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; +import * as url from 'url'; import { CanonicalCode } from '@opentelemetry/types'; import { RequestOptions } from 'https'; import { IgnoreMatcher } from '../src/types'; @@ -64,24 +65,50 @@ describe('Utils', () => { assert.strictEqual(result, true); }); }); - describe('isSatisfyPattern()', () => { + + describe('getIncomingOptions()', () => { + it('should get options object', () => { + const options = Object.assign( + { headers: { Expect: '100-continue' } }, + url.parse('http://google.fr/') + ); + const result = Utils.getIncomingOptions(options); + assert.strictEqual(result.hostname, 'google.fr'); + assert.strictEqual(result.headers!.Expect, options.headers.Expect); + assert.strictEqual(result.protocol, 'http:'); + assert.strictEqual(result.path, '/'); + assert.strictEqual((result as url.URL).pathname, '/'); + }); + }); + + describe('getRequestInfo()', () => { + it('should get options object', () => { + const result = Utils.getRequestInfo('http://google.fr/'); + assert.strictEqual(result.optionsParsed.hostname, 'google.fr'); + assert.strictEqual(result.optionsParsed.protocol, 'http:'); + assert.strictEqual(result.optionsParsed.path, '/'); + assert.strictEqual(result.pathname, '/'); + }); + }); + + describe('satisfiesPattern()', () => { it('string pattern', () => { - const answer1 = Utils.isSatisfyPattern('/test/1', {}, '/test/1'); + const answer1 = Utils.satisfiesPattern('/test/1', {}, '/test/1'); assert.strictEqual(answer1, true); - const answer2 = Utils.isSatisfyPattern('/test/1', {}, '/test/11'); + const answer2 = Utils.satisfiesPattern('/test/1', {}, '/test/11'); assert.strictEqual(answer2, false); }); it('regex pattern', () => { - const answer1 = Utils.isSatisfyPattern('/TeSt/1', {}, /\/test/i); + const answer1 = Utils.satisfiesPattern('/TeSt/1', {}, /\/test/i); assert.strictEqual(answer1, true); - const answer2 = Utils.isSatisfyPattern('/2/tEst/1', {}, /\/test/); + const answer2 = Utils.satisfiesPattern('/2/tEst/1', {}, /\/test/); assert.strictEqual(answer2, false); }); it('should throw if type is unknown', () => { try { - Utils.isSatisfyPattern( + Utils.satisfiesPattern( '/TeSt/1', {}, (true as unknown) as IgnoreMatcher<{}> @@ -93,14 +120,14 @@ describe('Utils', () => { }); it('function pattern', () => { - const answer1 = Utils.isSatisfyPattern( + const answer1 = Utils.satisfiesPattern( '/test/home', { headers: {} }, (url: string, req: { headers: unknown }) => req.headers && url === '/test/home' ); assert.strictEqual(answer1, true); - const answer2 = Utils.isSatisfyPattern( + const answer2 = Utils.satisfiesPattern( '/test/home', { headers: {} }, (url: string, req: { headers: unknown }) => url !== '/test/home' @@ -111,7 +138,7 @@ describe('Utils', () => { describe('isIgnored()', () => { beforeEach(() => { - Utils.isSatisfyPattern = sinon.spy(); + Utils.satisfiesPattern = sinon.spy(); }); afterEach(() => { @@ -122,7 +149,7 @@ describe('Utils', () => { const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); assert.strictEqual( - (Utils.isSatisfyPattern as sinon.SinonSpy).callCount, + (Utils.satisfiesPattern as sinon.SinonSpy).callCount, 1 ); }); @@ -131,7 +158,7 @@ describe('Utils', () => { const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); assert.strictEqual( - (Utils.isSatisfyPattern as sinon.SinonSpy).callCount, + (Utils.satisfiesPattern as sinon.SinonSpy).callCount, 1 ); }); @@ -139,7 +166,7 @@ describe('Utils', () => { it('should not call isSatisfyPattern', () => { Utils.isIgnored('/test/1', {}, []); assert.strictEqual( - (Utils.isSatisfyPattern as sinon.SinonSpy).callCount, + (Utils.satisfiesPattern as sinon.SinonSpy).callCount, 0 ); }); diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts new file mode 100644 index 0000000000..cffc62f494 --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -0,0 +1,23 @@ +import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; +import * as http from 'http'; + +export class DummyPropagation implements HttpTextFormat { + static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; + static SPAN_CONTEXT_KEY = 'x-dummy-span-id'; + extract(format: string, carrier: unknown): SpanContext { + return { + traceId: 'ca35dd5daef1401de22bcee7b7069195', + spanId: DummyPropagation.SPAN_CONTEXT_KEY, + }; + } + inject( + spanContext: SpanContext, + format: string, + headers: http.IncomingHttpHeaders + ): void { + headers[DummyPropagation.TRACE_CONTEXT_KEY] = + spanContext.traceId || 'undefined'; + headers[DummyPropagation.SPAN_CONTEXT_KEY] = + spanContext.spanId || 'undefined'; + } +} diff --git a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts new file mode 100644 index 0000000000..af34f5b50c --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts @@ -0,0 +1,39 @@ +import { SpanAuditProcessor } from './SpanAuditProcessor'; +import { + Span, + Tracer, + SpanOptions, + BinaryFormat, + HttpTextFormat, +} from '@opentelemetry/types'; + +export class ProxyTracer implements Tracer { + private readonly _tracer: Tracer; + constructor(tracer: Tracer, public audit: SpanAuditProcessor) { + this._tracer = tracer; + } + getCurrentSpan(): Span { + return this._tracer.getCurrentSpan(); + } + startSpan(name: string, options?: SpanOptions | undefined): Span { + const span = this._tracer.startSpan(name, options); + this.audit.push(span); + return span; + } + withSpan ReturnType>( + span: Span, + fn: T + ): ReturnType { + return this._tracer.withSpan(span, fn); + } + recordSpanData(span: Span): void {} + getBinaryFormat(): BinaryFormat { + throw new Error('Method not implemented.'); + } + getHttpTextFormat(): HttpTextFormat { + return this._tracer.getHttpTextFormat(); + } + wrapEmitter(emitter: unknown): void { + this._tracer.wrapEmitter(emitter); + } +} diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts new file mode 100644 index 0000000000..b2562777cb --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts @@ -0,0 +1,23 @@ +import { + SpanKind, + Status, + Attributes, + SpanContext, + Link, + Event, +} from '@opentelemetry/types'; + +// } +export interface SpanAudit { + spanContext: SpanContext; + attributes: Attributes; + name: string; + ended: boolean; + events: Event[]; + links: Link[]; + parentId?: string; + kind: SpanKind; + status: Status; + startTime: number; + endTime: number; +} diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts new file mode 100644 index 0000000000..53749a2d7b --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts @@ -0,0 +1,40 @@ +import { Span } from '@opentelemetry/types'; +import { SpanAudit } from './SpanAudit'; + +export class SpanAuditProcessor { + private static skipFields = ['_tracer']; + private _spans: Span[]; + constructor() { + this._spans = []; + } + + push(span: Span) { + this._spans.push(span); + } + + processSpans(): SpanAudit[] { + return this._spans.map(span => { + const auditSpan = {} as SpanAudit; + // TODO: use getter or SpanData once available + for (const key in span) { + if ( + span.hasOwnProperty(key) && + !SpanAuditProcessor.skipFields.includes(key) + ) { + /* tslint:disable:no-any */ + (auditSpan as any)[key.replace('_', '')] = + typeof (span as any)[key] === 'object' && + !Array.isArray((span as any)[key]) + ? { ...(span as any)[key] } + : (span as any)[key]; + /* tslint:enable:no-any */ + } + } + return auditSpan; + }); + } + + reset() { + this._spans = []; + } +} diff --git a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts new file mode 100644 index 0000000000..821e9db030 --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts @@ -0,0 +1,72 @@ +import { SpanKind } from '@opentelemetry/types'; +import * as assert from 'assert'; +import * as http from 'http'; +import { Attributes } from '../../src/enums/attributes'; +import { HttpPlugin } from '../../src/http'; +import { Utils } from '../../src/utils'; +import { DummyPropagation } from './DummyPropagation'; +import { SpanAudit } from './SpanAudit'; + +export const assertSpan = ( + span: SpanAudit, + kind: SpanKind, + validations: { + httpStatusCode: number; + httpMethod: string; + resHeaders: http.IncomingHttpHeaders; + reqHeaders: http.OutgoingHttpHeaders; + hostname: string; + pathname: string; + path?: string; + } +) => { + assert.strictEqual(span.spanContext.traceId.length, 32); + assert.strictEqual(span.spanContext.spanId.length, 16); + assert.strictEqual(span.kind, kind); + assert.strictEqual( + span.name, + `${validations.httpMethod} ${validations.pathname}` + ); + assert.strictEqual( + span.attributes[Attributes.COMPONENT], + HttpPlugin.component + ); + assert.strictEqual( + span.attributes[Attributes.HTTP_ERROR_MESSAGE], + span.status.message + ); + assert.strictEqual( + span.attributes[Attributes.HTTP_HOSTNAME], + validations.hostname + ); + assert.strictEqual( + span.attributes[Attributes.HTTP_METHOD], + validations.httpMethod + ); + assert.strictEqual( + span.attributes[Attributes.HTTP_PATH], + validations.path || validations.pathname + ); + assert.strictEqual( + span.attributes[Attributes.HTTP_STATUS_CODE], + validations.httpStatusCode + ); + assert.strictEqual(span.ended, true); + assert.strictEqual(span.links.length, 0); + assert.strictEqual(span.events.length, 0); + assert.deepStrictEqual( + span.status, + Utils.parseResponseStatus(validations.httpStatusCode) + ); + assert.ok(span.startTime < span.endTime); + assert.ok(span.endTime > 0); + + const userAgent = validations.reqHeaders['user-agent']; + if (userAgent) { + assert.strictEqual(span.attributes[Attributes.HTTP_USER_AGENT], userAgent); + } + + if (span.kind === SpanKind.SERVER) { + assert.strictEqual(span.parentId, DummyPropagation.SPAN_CONTEXT_KEY); + } +}; diff --git a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts new file mode 100644 index 0000000000..46cd74fe95 --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts @@ -0,0 +1,48 @@ +import * as http from 'http'; +import * as url from 'url'; +import { RequestOptions } from 'https'; + +export const httpRequest = { + get: ( + options: string | RequestOptions + ): Promise<{ + data: string; + statusCode: number | undefined; + resHeaders: http.IncomingHttpHeaders; + reqHeaders: http.OutgoingHttpHeaders; + method: string | undefined; + }> => { + const _options = + typeof options === 'string' + ? Object.assign(url.parse(options), { + headers: { + 'user-agent': 'http-plugin-test', + }, + }) + : options; + return new Promise((resolve, reject) => { + return http.get(_options, (resp: http.IncomingMessage) => { + const res = (resp as unknown) as http.IncomingMessage & { + req: http.IncomingMessage; + }; + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve({ + data, + statusCode: res.statusCode, + /* tslint:disable-next-line:no-any */ + reqHeaders: (res.req as any)._headers, + resHeaders: res.headers, + method: res.req.method, + }); + }); + resp.on('error', err => { + reject(err); + }); + }); + }); + }, +}; diff --git a/packages/opentelemetry-types/src/trace/tracer.ts b/packages/opentelemetry-types/src/trace/tracer.ts index 5c980c7d85..ec5b0b6106 100644 --- a/packages/opentelemetry-types/src/trace/tracer.ts +++ b/packages/opentelemetry-types/src/trace/tracer.ts @@ -96,4 +96,16 @@ export interface Tracer { * W3C Trace Context. */ getHttpTextFormat(): HttpTextFormat; + + /** + * Binds the trace context to the given event emitter. + * This is necessary in order to create child spans correctly in event + * handlers. + * @todo: Pending API discussion. Revisit if we should simply expose scopeManager instead of exposing methods through the tracer. + * Also Should we replace unknown by EventEmitter ? + * ref: https://github.com/Gozala/events + * @param emitter An event emitter whose handlers should have + * the trace context binded to them. + */ + wrapEmitter(emitter: unknown): void; } From 96732c1492d305209bd6ab29b114820be93e0744 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Thu, 8 Aug 2019 19:29:40 -0400 Subject: [PATCH 08/18] fix: add missing header and revert scopeManager to private field test: rename some tests fix: copy/paste tests Signed-off-by: Olivier Albertini --- .../test/http-enable.test.ts | 2 +- .../test/utils.test.ts | 23 +++++++++++-------- .../test/utils/DummyPropagation.ts | 16 +++++++++++++ .../test/utils/ProxyTracer.ts | 16 +++++++++++++ .../test/utils/SpanAudit.ts | 17 +++++++++++++- .../test/utils/SpanAuditProcessor.ts | 16 +++++++++++++ .../test/utils/assertSpan.ts | 16 +++++++++++++ .../test/utils/httpRequest.ts | 16 +++++++++++++ 8 files changed, 110 insertions(+), 12 deletions(-) diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/http-enable.test.ts index 78b5a2dd2e..2c25116e80 100644 --- a/packages/opentelemetry-plugin-http/test/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-enable.test.ts @@ -280,7 +280,7 @@ describe('HttpPlugin', () => { }); } - it('should create a rootSpan for GET requests and add propagation headers', async () => { + it('should create a span for GET requests and add propagation headers', async () => { nock.enableNetConnect(); const spans = audit.processSpans(); assert.strictEqual(spans.length, 0); diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts index f4f32cadc4..b91dbcbad3 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -137,33 +137,36 @@ describe('Utils', () => { }); describe('isIgnored()', () => { + let satisfiesPatternSpy: sinon.SinonSpy; beforeEach(() => { - Utils.satisfiesPattern = sinon.spy(); + satisfiesPatternSpy = sinon.spy(Utils, 'satisfiesPattern'); }); afterEach(() => { sinon.restore(); }); - it('should call isSatisfyPattern, n match', () => { - const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); - assert.strictEqual(answer1, false); + it('should call satisfiesPattern', () => { + Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual( (Utils.satisfiesPattern as sinon.SinonSpy).callCount, 1 ); }); - it('should call isSatisfyPattern, match', () => { + it('should call satisfiesPattern, match', () => { + satisfiesPatternSpy.restore(); + const answer1 = Utils.isIgnored('/test/1', {}, ['/test/1']); + assert.strictEqual(answer1, true); + }); + + it('should call satisfiesPattern, no match', () => { + satisfiesPatternSpy.restore(); const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); - assert.strictEqual( - (Utils.satisfiesPattern as sinon.SinonSpy).callCount, - 1 - ); }); - it('should not call isSatisfyPattern', () => { + it('should not call satisfiesPattern', () => { Utils.isIgnored('/test/1', {}, []); assert.strictEqual( (Utils.satisfiesPattern as sinon.SinonSpy).callCount, diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index cffc62f494..217ae67308 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 { SpanContext, HttpTextFormat } from '@opentelemetry/types'; import * as http from 'http'; diff --git a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts index af34f5b50c..467c4c3685 100644 --- a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts +++ b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 { SpanAuditProcessor } from './SpanAuditProcessor'; import { Span, diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts index b2562777cb..c8173a6fca 100644 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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, Status, @@ -7,7 +23,6 @@ import { Event, } from '@opentelemetry/types'; -// } export interface SpanAudit { spanContext: SpanContext; attributes: Attributes; diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts index 53749a2d7b..a07ab892d2 100644 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 { Span } from '@opentelemetry/types'; import { SpanAudit } from './SpanAudit'; diff --git a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts index 821e9db030..a1825f666a 100644 --- a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 } from '@opentelemetry/types'; import * as assert from 'assert'; import * as http from 'http'; diff --git a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts index 46cd74fe95..88d41c2e6d 100644 --- a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts +++ b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 * as http from 'http'; import * as url from 'url'; import { RequestOptions } from 'https'; From 8bb5afe75d5e30d4f5592af2247405b65449c968 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Fri, 9 Aug 2019 22:55:37 -0400 Subject: [PATCH 09/18] fix: add tests and improve attributes from spec fix parentSpanId instead of parentId from rebase add workaround with got and node12+ (real http call) improve args passed to function (url, options, cb) Signed-off-by: Olivier Albertini --- .../opentelemetry-plugin-http/package.json | 11 +- .../opentelemetry-plugin-http/src/http.ts | 37 +++--- .../opentelemetry-plugin-http/src/types.ts | 9 ++ .../opentelemetry-plugin-http/src/utils.ts | 4 +- .../test/fixtures/google.json | 43 +++++++ .../test/http-disable.test.ts | 5 +- .../test/http-enable.test.ts | 13 +- .../test/http-package.test.ts | 113 ++++++++++++++++++ .../test/utils.test.ts | 23 ++-- .../test/utils/DummyPropagation.ts | 16 --- .../test/utils/ProxyTracer.ts | 16 --- .../test/utils/SpanAudit.ts | 19 +-- .../test/utils/SpanAuditProcessor.ts | 16 --- .../test/utils/assertSpan.ts | 32 ++--- .../test/utils/httpRequest.ts | 16 --- 15 files changed, 228 insertions(+), 145 deletions(-) create mode 100644 packages/opentelemetry-plugin-http/test/fixtures/google.json create mode 100644 packages/opentelemetry-plugin-http/test/http-package.test.ts diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index 474dd1372b..0db289a6f7 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -43,9 +43,18 @@ "@types/node": "^12.6.9", "@types/semver": "^6.0.1", "@types/shimmer": "^1.0.1", - "@types/sinon":"^7.0.13", + "@types/sinon": "^7.0.13", + "@types/axios": "0.14.0", + "@types/got": "9.6.6", + "@types/superagent": "4.1.3", + "@types/request-promise-native": "1.0.16", "@opentelemetry/scope-async-hooks": "^0.0.1", "@opentelemetry/scope-base": "^0.0.1", + "axios": "^0.19.0", + "got": "^9.6.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "superagent": "5.1.0", "codecov": "^3.5.0", "gts": "^1.0.0", "mocha": "^6.2.0", diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 93dc707300..b671a905cb 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -36,9 +36,9 @@ import { HttpPluginConfig, Http, Func, - HttpCallback, ResponseEndArgs, ParsedRequestOptions, + HttpRequestArgs, } from './types'; import { Format } from './enums/format'; import { Attributes } from './enums/attributes'; @@ -157,13 +157,12 @@ export class HttpPlugin extends BasePlugin { // simply follow the latter. Ref: // https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback // https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/plugins/plugin-http.ts#L198 - return function outgoingGetRequest( - options: string | RequestOptions | URL, - callback?: HttpCallback - ) { - const req = request(options, callback); + return function outgoingGetRequest< + T extends RequestOptions | string | URL + >(options: T, ...args: HttpRequestArgs) { + const req = request(options, ...args); req.end(); - return req; + return req as ClientRequest; }; }; } @@ -204,9 +203,7 @@ export class HttpPlugin extends BasePlugin { const host = opts.hostname || opts.host || 'localhost'; span.setAttributes({ - [Attributes.HTTP_URL]: `${opts.protocol}//${opts.hostname}${ - (opts as url.UrlWithParsedQuery).pathname - }`, + [Attributes.HTTP_URL]: `${opts.protocol}//${opts.hostname}${opts.path}`, [Attributes.HTTP_HOSTNAME]: host, [Attributes.HTTP_METHOD]: method, [Attributes.HTTP_PATH]: opts.path || '/', @@ -257,12 +254,16 @@ export class HttpPlugin extends BasePlugin { const request = args[0] as IncomingMessage; const response = args[1] as ServerResponse; - const path = request.url ? url.parse(request.url).pathname || '/' : '/'; + const pathname = request.url + ? url.parse(request.url).pathname || '/' + : '/'; const method = request.method || 'GET'; plugin._logger.debug('%s plugin incomingRequest', plugin.moduleName); - if (Utils.isIgnored(path, request, plugin.options.ignoreIncomingPaths)) { + if ( + Utils.isIgnored(pathname, request, plugin.options.ignoreIncomingPaths) + ) { return original.apply(this, [event, ...args]); } @@ -278,7 +279,7 @@ export class HttpPlugin extends BasePlugin { } const span = plugin._spanFactory.createInstance( - `${method} ${path}`, + `${method} ${pathname}`, spanOptions ); @@ -316,8 +317,8 @@ export class HttpPlugin extends BasePlugin { if (requestUrl) { span.setAttributes({ - [Attributes.HTTP_PATH]: requestUrl.pathname || '/', - [Attributes.HTTP_ROUTE]: requestUrl.path || '/', + [Attributes.HTTP_PATH]: requestUrl.path || '/', + [Attributes.HTTP_ROUTE]: requestUrl.pathname || '/', }); } @@ -351,10 +352,10 @@ export class HttpPlugin extends BasePlugin { return function outgoingRequest( this: {}, options: RequestOptions | string, - callback?: HttpCallback + ...args: unknown[] ): ClientRequest { if (!options) { - return original.apply(this, [options, callback]); + return original.apply(this, [options, ...args]); } const { origin, pathname, method, optionsParsed } = Utils.getRequestInfo( @@ -362,7 +363,7 @@ export class HttpPlugin extends BasePlugin { ); const request: ClientRequest = original.apply(this, [ optionsParsed, - callback, + ...args, ]); if ( diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index a9c43cc294..6322508dd1 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -32,6 +32,15 @@ export type IgnoreMatcher = export type HttpCallback = (res: IncomingMessage) => void; export type RequestFunction = typeof request; export type GetFunction = typeof get; + +export type HttpCallbackOptional = HttpCallback | undefined; + +// from node 10+ +export type RequestSignature = [http.RequestOptions, HttpCallbackOptional] & + HttpCallback; + +export type HttpRequestArgs = Array; + export type ParsedRequestOptions = | http.RequestOptions & Partial | http.RequestOptions; diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 22a1a60137..6ec0716a78 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -41,9 +41,9 @@ export class Utils { } return requestUrl.href && requestUrl.href.startsWith('http') - ? `${requestUrl.protocol}//${requestUrl.hostname}${requestUrl.pathname}` + ? `${requestUrl.protocol}//${requestUrl.hostname}${requestUrl.path}` : `${requestUrl.protocol || 'http:'}//${headers.host || - 'localhost'}${requestUrl.pathname || '/'}`; + 'localhost'}${requestUrl.path || '/'}`; } /** * Parse status code from HTTP response. diff --git a/packages/opentelemetry-plugin-http/test/fixtures/google.json b/packages/opentelemetry-plugin-http/test/fixtures/google.json new file mode 100644 index 0000000000..98ec958a2f --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/fixtures/google.json @@ -0,0 +1,43 @@ +[ + { + "scope": "http://www.google.com", + "method": "GET", + "path": "/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8", + "body": "", + "status": 200, + "response": "3c21646f63747970652068746d6c3e3c68746d6c206c616e673d22656e2d4341223e3c686561643e3c6d65746120636861727365743d225554462d38223e3c6d65746120636f6e74656e743d222f696d616765732f6272616e64696e672f676f6f676c65672f31782f676f6f676c65675f7374616e646172645f636f6c6f725f31323864702e706e6722206974656d70726f703d22696d616765223e3c7469746c653e6178696f73202d20476f6f676c65205365617263683c2f7469746c653e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220613d77696e646f772e706572666f726d616e63653b77696e646f772e73746172743d286e65772044617465292e67657454696d6528293b613a7b76617220623d77696e646f773b69662861297b76617220633d612e74696d696e673b69662863297b76617220643d632e6e617669676174696f6e53746172742c653d632e726573706f6e736553746172743b696628653e642626653c3d77696e646f772e7374617274297b77696e646f772e73746172743d653b622e777372743d652d643b627265616b20617d7d612e6e6f77262628622e777372743d4d6174682e666c6f6f7228612e6e6f77282929297d7d77696e646f772e676f6f676c653d77696e646f772e676f6f676c657c7c7b7d3b676f6f676c652e6166743d66756e6374696f6e2866297b662e7365744174747269627574652822646174612d696d6c222c2b6e65772044617465297d3b7d292e63616c6c2874686973293b2866756e6374696f6e28297b76617220633d5b5d2c653d303b77696e646f772e70696e673d66756e6374696f6e2862297b2d313d3d622e696e6465784f662822267a782229262628622b3d22267a783d222b286e65772044617465292e67657454696d652829293b76617220613d6e657720496d6167652c643d652b2b3b635b645d3d613b612e6f6e6572726f723d612e6f6e6c6f61643d612e6f6e61626f72743d66756e6374696f6e28297b64656c65746520635b645d7d3b612e7372633d627d3b7d292e63616c6c2874686973293b3c2f7363726970743e3c7374796c653e626f64797b6d617267696e3a30206175746f3b6d61782d77696474683a37333670783b70616464696e673a30203870787d617b636f6c6f723a233139363744323b746578742d6465636f726174696f6e3a6e6f6e653b7461702d686967686c696768742d636f6c6f723a7267626128302c302c302c2e31297d613a766973697465647b636f6c6f723a233442313141387d613a686f7665727b746578742d6465636f726174696f6e3a756e6465726c696e657d696d677b626f726465723a307d68746d6c7b666f6e742d66616d696c793a526f626f746f2c48656c7665746963614e6575652c417269616c2c73616e732d73657269663b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070783b746578742d73697a652d61646a7573743a313030253b636f6c6f723a233343343034333b776f72642d777261703a627265616b2d776f72643b6261636b67726f756e642d636f6c6f723a236666667d2e625273576e637b6261636b67726f756e642d636f6c6f723a236666663b626f726465722d746f703a31707820736f6c696420236530653065303b6865696768743a333970783b6f766572666c6f773a68696464656e7d2e4e365257567b6865696768743a353170783b6f766572666c6f772d7363726f6c6c696e673a746f7563683b6f766572666c6f772d783a6175746f3b6f766572666c6f772d793a68696464656e7d2e5576363771627b626f782d7061636b3a6a7573746966793b666f6e742d73697a653a313270783b6c696e652d6865696768743a333770783b6a7573746966792d636f6e74656e743a73706163652d6265747765656e3b6a7573746966792d636f6e74656e743a73706163652d6265747765656e7d2e55763637716220612c2e557636377162207370616e7b636f6c6f723a233735373537353b646973706c61793a626c6f636b3b666c65783a6e6f6e653b70616464696e673a3020313670783b746578742d616c69676e3a63656e7465723b746578742d7472616e73666f726d3a7570706572636173653b7d7370616e2e4f585875707b626f726465722d626f74746f6d3a32707820736f6c696420233432383566343b636f6c6f723a233432383566343b666f6e742d7765696768743a626f6c647d612e655a743878643a766973697465647b636f6c6f723a233735373537357d2e46456c6273667b626f726465722d6c6566743a31707820736f6c6964207267626128302c302c302c2e3132297d6865616465722061727469636c657b6f766572666c6f773a76697369626c657d2e5067373062667b6865696768743a333970783b646973706c61793a626f783b646973706c61793a666c65783b646973706c61793a666c65783b77696474683a313030257d2e4830505165637b706f736974696f6e3a72656c61746976653b666c65783a317d2e7362637b646973706c61793a666c65783b77696474683a313030257d2e50673730626620696e7075747b6d617267696e3a3270782034707820327078203870783b7d2e787b77696474683a323670783b636f6c6f723a233735373537353b666f6e743a323770782f3338707820617269616c2c2073616e732d73657269663b6c696e652d6865696768743a343070783b7d237164436c77627b666c65783a302030206175746f3b77696474683a333970783b6865696768743a333970783b626f726465722d626f74746f6d3a303b70616464696e673a303b626f726465722d746f702d72696768742d7261646975733a3870783b6261636b67726f756e642d636f6c6f723a233362373865373b626f726465723a31707820736f6c696420233333363764363b6261636b67726f756e642d696d6167653a75726c28646174613a696d6167652f6769663b6261736536342c52306c474f4464684a41416a41504948414f44722f6e436b2b4d505a2f466d5639367a4b2b2f372b2f354b352b6b714c39697741414141414a41416a41454144616e693633503477796b6d624b635152584473635141454d586d6d65614c51564c43756b7a79433039416a66654b37762f4d41616a41434c68504d564167776a73556345695a61387867415972567176324b783269777349414141426b6e664242414b7254453449634d796f743875723864617471496251664a646e41666f3257453642563035775849694a69676b414f773d3d293b7d2e73637b666f6e742d73697a653a3b706f736974696f6e3a6162736f6c7574653b746f703a333970783b6c6566743a303b72696768743a303b626f782d736861646f773a3070782032707820357078207267626128302c302c302c302e32293b7a2d696e6465783a323b6261636b67726f756e642d636f6c6f723a236666667d2e73633e6469767b70616464696e673a3130707820313070783b70616464696e672d6c6566743a313670783b70616464696e672d6c6566743a313470783b626f726465722d746f703a31707820736f6c696420234446453145357d2e7363737b6261636b67726f756e642d636f6c6f723a236635663566353b7d2e6e6f484978637b646973706c61793a626c6f636b3b666f6e742d73697a653a313670783b70616464696e673a3020302030203870783b666c65783a313b6865696768743a333570783b6f75746c696e653a6e6f6e653b626f726465723a6e6f6e653b77696474683a313030253b2d7765626b69742d7461702d686967686c696768742d636f6c6f723a7267626128302c302c302c30293b6f766572666c6f773a68696464656e3b7d2e73626320696e7075745b747970653d746578745d7b6261636b67726f756e643a6e6f6e657d2e736d6c202e634f6c3449647b646973706c61793a6e6f6e657d2e6c7b646973706c61793a6e6f6e657d2e736d6c206865616465727b6261636b67726f756e643a6e6f6e657d2e736d6c202e6c7b646973706c61793a626c6f636b3b70616464696e673a30203870787d2e736d6c202e6c7b6c65747465722d73706163696e673a2d3170783b746578742d616c69676e3a63656e7465723b626f726465722d7261646975733a3270782030203020303b666f6e743a323270782f33367078204675747572612c20417269616c2c2073616e732d73657269663b666f6e742d736d6f6f7468696e673a616e7469616c69617365647d2e627a316c42627b6261636b67726f756e643a236666663b626f726465722d7261646975733a38707820387078203020303b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3138293b6d617267696e2d746f703a313070787d2e4b50374c43627b626f726465722d7261646975733a30203020387078203870783b626f782d736861646f773a30203270782033707820726762612833322c2033332c2033362c20302e3138293b6d617267696e2d626f74746f6d3a313070783b6f766572666c6f773a68696464656e7d2e634f6c3449647b6c65747465722d73706163696e673a2d3170783b746578742d616c69676e3a63656e7465723b666f6e743a32327074204675747572612c20417269616c2c2073616e732d73657269663b70616464696e673a3130707820302035707820303b6865696768743a333770783b666f6e742d736d6f6f7468696e673a616e7469616c69617365647d2e634f6c344964207370616e7b646973706c61793a696e6c696e652d626c6f636b7d2e533539316a7b6865696768743a313030257d2e5636677756647b636f6c6f723a233432383546347d2e69576b7576647b636f6c6f723a234541343333357d2e63447251377b636f6c6f723a236662636330357d2e6e746c52397b636f6c6f723a233334413835337d2e744a334d79637b2d7765626b69742d7472616e73666f726d3a726f74617465282d3230646567293b706f736974696f6e3a72656c61746976653b6c6566743a2d3170783b646973706c61793a696e6c696e652d626c6f636b7d666f6f7465727b746578742d616c69676e3a63656e7465723b6d617267696e2d746f703a313870787d666f6f74657220612c666f6f74657220613a766973697465642c2e736d695562627b636f6c6f723a233566363336387d2e6b73545534637b6d617267696e3a3020313370787d236d436c6a6f627b6d617267696e2d746f703a333670787d236d436c6a6f623e6469767b6d617267696e3a323070787d3c2f7374796c653e3c2f686561643e3c626f6479206a736d6f64656c3d2220223e3c6865616465722069643d22686472223e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220633d3530303b2866756e6374696f6e28297b77696e646f772e73637265656e262677696e646f772e73637265656e2e77696474683c3d63262677696e646f772e73637265656e2e6865696768743c3d632626646f63756d656e742e676574456c656d656e7442794964282268647222292e636c6173734c6973742e6164642822736d6c22293b7d292e63616c6c2874686973293b7d2928293b3c2f7363726970743e3c64697620636c6173733d22634f6c344964223e3c6120687265663d222f3f73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673514f776743223e3c7370616e20636c6173733d22563667775664223e473c2f7370616e3e3c7370616e20636c6173733d2269576b757664223e6f3c2f7370616e3e3c7370616e20636c6173733d226344725137223e6f3c2f7370616e3e3c7370616e20636c6173733d22563667775664223e673c2f7370616e3e3c7370616e20636c6173733d226e746c5239223e6c3c2f7370616e3e3c7370616e20636c6173733d2269576b75766420744a334d7963223e653c2f7370616e3e3c2f613e3c2f6469763e3c64697620636c6173733d22627a316c4262223e3c666f726d20636c6173733d22506737306266222069643d227366223e3c6120636c6173733d226c2220687265663d222f3f6f75747075743d73656172636826616d703b69653d5554462d3826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735150416745223e3c7370616e20636c6173733d22563667775664223e473c2f7370616e3e3c7370616e20636c6173733d2269576b757664223e6f3c2f7370616e3e3c7370616e20636c6173733d226344725137223e6f3c2f7370616e3e3c7370616e20636c6173733d22563667775664223e673c2f7370616e3e3c7370616e20636c6173733d226e746c5239223e6c3c2f7370616e3e3c7370616e20636c6173733d2269576b75766420744a334d7963223e653c2f7370616e3e3c2f613e3c696e707574206e616d653d226965222076616c75653d2249534f2d383835392d312220747970653d2268696464656e223e3c64697620636c6173733d22483050516563223e3c64697620636c6173733d227362632065736263223e3c696e70757420636c6173733d226e6f48497863222076616c75653d226178696f7322206175746f6361706974616c697a653d226e6f6e6522206175746f636f6d706c6574653d226f666622206e616d653d227122207370656c6c636865636b3d2266616c73652220747970653d2274657874223e3c696e707574206e616d653d226f712220747970653d2268696464656e223e3c696e707574206e616d653d226171732220747970653d2268696464656e223e3c64697620636c6173733d2278223ed73c2f6469763e3c64697620636c6173733d227363223e3c2f6469763e3c2f6469763e3c2f6469763e3c627574746f6e2069643d227164436c77622220747970653d227375626d6974223e3c2f627574746f6e3e3c2f666f726d3e3c2f6469763e3c6e6f7363726970743e3c6d65746120636f6e74656e743d22303b75726c3d2f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b6762763d3126616d703b7365693d704d524f586250624472505339414f426d4b505942512220687474702d65717569763d2272656672657368223e3c7374796c653e7461626c652c6469762c7370616e2c707b646973706c61793a6e6f6e657d3c2f7374796c653e3c646976207374796c653d22646973706c61793a626c6f636b223e506c6561736520636c69636b203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b6762763d3126616d703b7365693d704d524f586250624472505339414f426d4b50594251223e686572653c2f613e20696620796f7520617265206e6f7420726564697265637465642077697468696e206120666577207365636f6e64732e3c2f6469763e3c2f6e6f7363726970743e3c2f6865616465723e3c6469762069643d226d61696e223e3c6469763e3c64697620636c6173733d224b50374c4362223e203c64697620636c6173733d22625273576e63223e203c64697620636c6173733d224e36525756223e203c64697620636c6173733d2250673730626620557636377162223e203c7370616e20636c6173733d224f58587570223e416c6c3c2f7370616e3e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d6e777326616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943436742223e4e6577733c2f613e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d76696426616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943536743223e566964656f733c2f613e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d6973636826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943696744223e496d616765733c2f613e2020203c6120687265663d22687474703a2f2f6d6170732e676f6f676c652e636f6d2f6d6170733f713d6178696f7326616d703b756d3d3126616d703b69653d5554462d3826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943796745223e4d6170733c2f613e20203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d73686f7026616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554944436746223e53686f7070696e673c2f613e20203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d626b7326616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554944536747223e426f6f6b733c2f613e20202020203c64697620636c6173733d2246456c627366223e3c6120687265663d222f616476616e6365645f73656172636822207374796c653d2277686974652d73706163653a6e6f77726170222069643d2273742d746f67676c652220726f6c653d22627574746f6e223e53656172636820746f6f6c733c2f613e3c2f6469763e203c2f6469763e203c2f6469763e203c2f6469763e203c2f6469763e3c64697620636c6173733d22506737306266207745736a6264205a494e62626320787064204f396735636320755550476922207374796c653d22646973706c61793a6e6f6e65222069643d2273742d63617264223e3c7374796c653e2e7745736a62647b6261636b67726f756e642d636f6c6f723a236666663b6865696768743a343470783b77686974652d73706163653a6e6f777261707d2e636f505538637b6865696768743a363070783b6f766572666c6f772d7363726f6c6c696e673a746f7563683b6f766572666c6f772d783a6175746f3b6f766572666c6f772d793a68696464656e7d2e586a326175657b6865696768743a343470783b6f766572666c6f773a68696464656e7d2e526e4e477a657b6d617267696e3a3131707820313670787d2e7745736a6264206469762c2e7745736a626420612c2e7745736a6264206c697b6f75746c696e652d77696474683a303b6f75746c696e653a6e6f6e657d3c2f7374796c653e3c64697620636c6173733d22586a32617565223e3c64697620636c6173733d22636f50553863223e3c64697620636c6173733d22526e4e477a65223e3c7374796c653e2e5041394a357b646973706c61793a696e6c696e652d626c6f636b7d2e5258614f66647b646973706c61793a696e6c696e652d626c6f636b3b6865696768743a323270783b706f736974696f6e3a72656c61746976653b70616464696e672d746f703a303b70616464696e672d626f74746f6d3a303b70616464696e672d72696768743a313670783b70616464696e672d6c6566743a303b6c696e652d6865696768743a323270783b637572736f723a706f696e7465723b746578742d7472616e73666f726d3a7570706572636173653b666f6e742d73697a653a313270783b636f6c6f723a233735373537357d2e736131746f637b646973706c61793a6e6f6e653b706f736974696f6e3a6162736f6c7574653b6261636b67726f756e643a236666663b626f726465723a31707820736f6c696420236436643664363b626f782d736861646f773a302032707820347078207267626128302c302c302c302e33293b6d617267696e3a303b77686974652d73706163653a6e6f777261703b7a2d696e6465783a3130333b6c696e652d6865696768743a313770783b70616464696e672d746f703a3570783b70616464696e672d626f74746f6d3a3570783b70616464696e672d6c6566743a3070787d2e5041394a353a686f766572202e736131746f637b646973706c61793a626c6f636b7d2e6d475379386420613a6163746976652c2e5258614f66643a6163746976657b636f6c6f723a233432383566347d3c2f7374796c653e3c64697620636c6173733d225041394a35223e3c64697620636c6173733d225258614f66642220726f6c653d22627574746f6e2220746162696e6465783d2230223e3c7374796c653e2e54574d4f55637b646973706c61793a696e6c696e652d626c6f636b3b70616464696e672d72696768743a313470783b77686974652d73706163653a6e6f777261707d2e7651597547667b666f6e742d7765696768743a626f6c647d2e4f6d54497a667b626f726465722d636f6c6f723a23393039303930207472616e73706172656e743b626f726465722d7374796c653a736f6c69643b626f726465722d77696474683a347078203470782030203470783b77696474683a303b6865696768743a303b6d617267696e2d6c6566743a2d313070783b746f703a3530253b6d617267696e2d746f703a2d3270783b706f736974696f6e3a6162736f6c7574657d2e5258614f66643a616374697665202e4f6d54497a667b626f726465722d636f6c6f723a23343238356634207472616e73706172656e747d3c2f7374796c653e3c64697620636c6173733d2254574d4f5563223e416e792074696d653c2f6469763e3c7370616e20636c6173733d224f6d54497a66223e3c2f7370616e3e3c2f6469763e3c756c20636c6173733d22736131746f63206f7a61744d223e3c7374796c653e2e6f7a61744d7b666f6e742d73697a653a313270783b746578742d7472616e73666f726d3a7570706572636173657d2e6f7a61744d202e794e46736c2c2e6f7a61744d206c697b6c6973742d7374796c652d747970653a6e6f6e653b6c6973742d7374796c652d706f736974696f6e3a6f7574736964653b6c6973742d7374796c652d696d6167653a6e6f6e657d2e794e46736c2e536b556a34632c2e794e46736c20617b636f6c6f723a7267626128302c302c302c302e3534293b746578742d6465636f726174696f6e3a6e6f6e653b70616464696e673a36707820343470782036707820313470783b6c696e652d6865696768743a313770783b646973706c61793a626c6f636b7d2e536b556a34637b6261636b67726f756e642d696d6167653a75726c282f2f73736c2e677374617469632e636f6d2f75692f76312f6d656e752f636865636b6d61726b322e706e67293b6261636b67726f756e642d706f736974696f6e3a72696768742063656e7465723b6261636b67726f756e642d7265706561743a6e6f2d7265706561747d2e536b556a34633a6163746976657b6261636b67726f756e642d636f6c6f723a236635663566357d3c2f7374796c653e3c6c6920636c6173733d22794e46736c20536b556a3463223e416e792074696d653c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494477223e5061737420686f75723c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6426616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494541223e5061737420323420686f7572733c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a7726616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494551223e50617374207765656b3c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6d26616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494567223e50617374206d6f6e74683c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a7926616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494577223e5061737420796561723c2f613e3c2f6c693e3c2f756c3e3c2f6469763e3c64697620636c6173733d225041394a35223e3c64697620636c6173733d225258614f66642220726f6c653d22627574746f6e2220746162696e6465783d2230223e3c64697620636c6173733d2254574d4f5563223e416c6c20726573756c74733c2f6469763e3c7370616e20636c6173733d224f6d54497a66223e3c2f7370616e3e3c2f6469763e3c756c20636c6173733d22736131746f63206f7a61744d223e3c6c6920636c6173733d22794e46736c20536b556a3463223e416c6c20726573756c74733c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d6c693a3126616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494651223e566572626174696d3c2f613e3c2f6c693e3c2f756c3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220613d646f63756d656e742e676574456c656d656e7442794964282273742d746f67676c6522292c623d646f63756d656e742e676574456c656d656e7442794964282273742d6361726422293b612626622626612e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e2863297b622e7374796c652e646973706c61793d622e7374796c652e646973706c61793f22223a226e6f6e65223b632e70726576656e7444656661756c7428297d2c2131293b7d292e63616c6c2874686973293b3c2f7363726970743e3c2f6469763e3c2f6469763e3c7374796c653e2e5a494e6262637b6261636b67726f756e642d636f6c6f723a236666663b6d617267696e2d626f74746f6d3a313070783b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238293b626f726465722d7261646975733a3870787d2e75555047697b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070783b7d2e4f39673563633e2a3a66697273742d6368696c647b626f726465722d746f702d6c6566742d7261646975733a3870783b626f726465722d746f702d72696768742d7261646975733a3870787d2e4f39673563633e2a3a6c6173742d6368696c647b626f726465722d626f74746f6d2d6c6566742d7261646975733a3870783b626f726465722d626f74746f6d2d72696768742d7261646975733a3870787d2e726c37696c627b646973706c61793a626c6f636b3b636c6561723a626f74687d2e6c634a4631647b6d617267696e2d6c6566743a313670783b666c6f61743a72696768743b7d2e6b437259547b70616464696e673a31327078203136707820313270787d612e6664597371667b636f6c6f723a233442313141387d2e424e656177657b77686974652d73706163653a7072652d6c696e653b776f72642d777261703a627265616b2d776f72647d2e76766a774a627b636f6c6f723a233139363744323b666f6e742d73697a653a313670783b6c696e652d6865696768743a323070787d2e76766a774a6220613a766973697465647b636f6c6f723a233442313141387d2e76766a774a622e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e76766a774a622e48724764656220613a766973697465647b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e55506d69747b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e55506d69742e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e55506d69742e415037576e647b636f6c6f723a7267626128302c3130322c33332c31297d2e7835346774667b6865696768743a3170783b6261636b67726f756e642d636f6c6f723a236466653165353b6d617267696e3a3020313670787d2e4170354f53647b70616464696e672d626f74746f6d3a313270787d2e7333763972647b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e7333763972642e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e7333763972642e415037576e647b636f6c6f723a233230323132347d2e6d53783145657b70616464696e672d6c6566743a343870783b6d617267696e3a307d2e7639693631657b70616464696e672d626f74746f6d3a3870787d2e584c6c6f58657b636f6c6f723a233139363744323b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e584c6c6f586520613a766973697465647b636f6c6f723a233442313141387d2e584c6c6f58652e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e584c6c6f58652e48724764656220613a766973697465647b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e5a54763942627b646973706c61793a626c6f636b7d2e6465497643627b666f6e742d73697a653a313670783b6c696e652d6865696768743a323070783b666f6e742d7765696768743a3430307d2e6465497643622e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e6465497643622e415037576e647b636f6c6f723a233230323132347d2e4643557030637b666f6e742d7765696768743a626f6c647d2e58374e5456657b646973706c61793a7461626c653b77696474683a313030253b70616464696e672d72696768743a313670783b626f782d73697a696e673a626f726465722d626f787d2e74486d6651657b646973706c61793a7461626c652d63656c6c3b70616464696e673a313270782030203132707820313670787d2e554874726b7b77696474683a373270787d2e4842544d36647b77696474683a333070787d2e5853377947647b646973706c61793a7461626c652d63656c6c3b746578742d616c69676e3a63656e7465723b766572746963616c2d616c69676e3a6d6964646c653b70616464696e673a3132707820302031327078203870787d2e616d335142667b646973706c61793a7461626c653b766572746963616c2d616c69676e3a746f707d2e5862355652657b636f6c6f723a233139363744327d613a76697369746564202e5862355652657b636f6c6f723a233442313141387d2e5862355652652e74723064777b636f6c6f723a72676261283235352c3235352c3235352c31297d613a76697369746564202e5862355652652e74723064777b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e74416438447b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e74416438442e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e74416438442e415037576e647b636f6c6f723a233730373537417d2e614a79694f637b636f6c6f723a233030363632317d2e6f754a3943627b666c6f61743a6c6566743b70616464696e672d72696768743a313670787d2e526f434c6e657b636c6561723a626f74687d2e45594f736c647b646973706c61793a696e6c696e652d626c6f636b3b706f736974696f6e3a72656c61746976657d2e424669395a627b6f766572666c6f773a68696464656e3b706f736974696f6e3a72656c61746976657d2e53374a647a657b616c69676e2d6974656d733a63656e7465723b646973706c61793a666c65783b666c65782d646972656374696f6e3a636f6c756d6e3b6a7573746966792d636f6e74656e743a73706163652d61726f756e647d2e58646c7230647b6f766572666c6f772d783a6175746f3b2d7765626b69742d6f766572666c6f772d7363726f6c6c696e673a746f7563683b6d617267696e3a30202d3870783b70616464696e673a313670782030203136707820323470783b70616464696e672d746f703a3270783b6d617267696e2d746f703a2d3270783b7472616e73666f726d3a7472616e736c617465336428302c302c30297d2e6964673862657b646973706c61793a7461626c653b626f726465722d636f6c6c617073653a73657061726174653b626f726465722d73706163696e673a38707820303b6d617267696e3a30202d3870783b70616464696e672d72696768743a313670787d2e425647304e627b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a746f703b6261636b67726f756e642d636f6c6f723a236666663b626f726465722d7261646975733a3870783b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238293b6f766572666c6f773a68696464656e7d2e55796b5439647b646973706c61793a626c6f636b3b666c6f61743a72696768743b70616464696e672d6c6566743a313670787d2e6e59543751627b636c6561723a626f74687d2e736b566770627b646973706c61793a7461626c653b7461626c652d6c61796f75743a66697865643b77696474683a313030257d2e5647484d58647b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a6d6964646c653b6865696768743a353270783b746578742d616c69676e3a63656e7465727d2e4c70614472627b6d617267696e3a30206175746f203870783b646973706c61793a626c6f636b7d2e766253684f657b70616464696e672d746f703a307d2e4156736570667b70616464696e672d626f74746f6d3a3870787d2e4156736570662e753278314f647b70616464696e672d626f74746f6d3a307d2e787063202e6877632c2e787078202e6877787b646973706c61793a6e6f6e657d2e5275386964627b6d617267696e2d746f703a2d313670787d2e70756e657a7b666f6e742d7765696768743a3730303b6c65747465722d73706163696e673a302e373570783b746578742d7472616e73666f726d3a7570706572636173657d2e7779727758637b666f6e742d73697a653a313270783b6c696e652d6865696768743a313670787d2e7779727758632e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e7779727758632e415037576e647b636f6c6f723a233230323132347d2e6d4868796c667b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a6d6964646c657d2e575a35474a667b616c69676e2d6974656d733a63656e7465723b70616464696e673a3020323070783b6d696e2d77696474683a31313270787d2e714e394b65642c2e44586b354d657b6d617267696e3a30206175746f7d2e44586b354d657b6d617267696e2d626f74746f6d3a313270787d2e51693946647b6261636b67726f756e643a236666663b626f726465723a303b626f726465722d7261646975733a39393970783b646973706c61793a626c6f636b3b6865696768743a353670783b6a7573746966792d636f6e74656e743a63656e7465723b77696474683a353670783b7a2d696e6465783a307d2e51693946647b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238292c696e7365742030203020302030207267626128302c302c302c302e3130292c696e73657420302030203020302072676261283235352c3235352c3235352c302e3530297d2e51693946643a666f6375737b6f75746c696e653a6e6f6e657d2e5169394664202e685748754a7b646973706c61793a626c6f636b3b6d617267696e3a30206175746f7d2e6a69356a70667b746578742d616c69676e3a63656e7465727d68727b626f726465723a303b626f726465722d626f74746f6d3a31707820736f6c696420236466653165353b6d617267696e3a303b7d2e425579624b652c2e48736e4642667b6d617267696e2d6c6566743a313670787d2e425579624b652c2e6f4d3247417b6d617267696e2d72696768743a313670787d2e584f377268637b6d617267696e3a30202d313670787d2e6949576d34627b626f782d73697a696e673a626f726465722d626f783b6d696e2d6865696768743a343870787d2e664c745873637b70616464696e673a313470783b746578742d616c69676e3a63656e7465727d2e4c796d38577b77696474683a313470783b6865696768743a323070783b706f736974696f6e3a72656c61746976653b6d617267696e3a30206175746f7d2e4164665872627b6d617267696e2d6c6566743a2d313470783b766572746963616c2d616c69676e3a6d6964646c653b646973706c61793a696e6c696e652d626c6f636b7d2e4c796d3857206469767b706f736974696f6e3a6162736f6c7574653b626f726465722d6c6566743a37707820736f6c6964207472616e73706172656e743b626f726465722d72696768743a37707820736f6c6964207472616e73706172656e743b77696474683a303b6865696768743a303b6c6566743a307d2e4979596145647b746f703a3770783b626f726465722d746f703a37707820736f6c696420233735373537357d2e4543554851657b746f703a3470783b626f726465722d746f703a37707820736f6c696420236666667d2e4165515175627b626f74746f6d3a3770783b626f726465722d626f74746f6d3a37707820736f6c696420233735373537357d2e5943553765627b626f74746f6d3a3470783b626f726465722d626f74746f6d3a37707820736f6c696420236666667d2e4963783643647b6d617267696e3a30206175746f203870787d2e6d41646a51637b746578742d616c69676e3a72696768747d2e75456563337b666f6e742d73697a653a313270783b6c696e652d6865696768743a313670787d2e75456563332e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e75456563332e415037576e647b636f6c6f723a233730373537417d2e724c736879662c2e426d503574667b70616464696e672d746f703a313270783b70616464696e672d626f74746f6d3a313270787d2e773143334c652c2e426d503574662c2e47354e6242647b70616464696e672d6c6566743a313670783b70616464696e672d72696768743a313670783b7d2e47354e6242647b70616464696e672d626f74746f6d3a313270787d2e6e4d796d65667b646973706c61793a666c65787d2e473565466c667b666c65783a313b646973706c61793a626c6f636b7d2e6e4d796d6566207370616e7b746578742d616c69676e3a63656e7465727d3c2f7374796c653e3c6469763e3c212d2d53575f435f582d2d3e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4141656751494278414226616d703b7573673d414f76566177327466496430584346385649396a682d324f35555069223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f733c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e6178696f732e636f6d3c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d224170354f5364223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e536d6172742c20656666696369656e74206e65777320776f72746879206f6620796f75722074696d652c20617474656e74696f6e2c20616e642074727573742e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f706f6c697469637326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417741586f4543416351417726616d703b7573673d414f765661773139685044464b57637a653030627974527851476576223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e506f6c69746963733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f617574686f72732f6e6577736465736b26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a424177416e6f4543416351425126616d703b7573673d414f7656617730315a6b4f30626d50386e4370657152386257474959223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e53746f72696573206279204178696f733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6178696f732d64617368626f6172642d313535303631393334332d64653734306436612d656163312d343539392d383262642d3434303533343537376639322e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417741336f4543416351427726616d703b7573673d414f76566177305658706145566f434f444153557657784f61466652223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e4178696f732044617368626f6172643c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6e6577736c65747465727326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417742486f4543416351435126616d703b7573673d414f76566177316255574b54564d7a4944436d736a34536d614c4755223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e4e6577736c6574746572733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f776f726c6426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417742586f4543416351437726616d703b7573673d414f765661773246776f732d5f6e416d39666e4963486a754b325a53223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e576f726c643c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f627573696e65737326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a424177426e6f4543416351445126616d703b7573673d414f76566177324366426a4a724a72716e5052623241754d614e4953223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e427573696e6573733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e546f702073746f726965733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6e6577736c6574746572732f6178696f732d616d2d61653065626362352d616432352d346531632d623662642d3138633530393464656330312e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351714f63424d4164364241674145414926616d703b7573673d414f765661773148445a7242345437756b327137384256725676426f223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d2272514d516f6420586235565265223e4178696f7320414d202d2041756775737420392c20323031393c2f7370616e3e3c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e3c7370616e20636c6173733d2272514d516f6420614a79694f63223e4178696f733c2f7370616e3e20b72031206461792061676f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f636f756e74726965732d6d6f73742d7269736b2d77617465722d6372697369732d66343066343866392d623032652d346536622d393965642d3136346335636239353333352e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351714f63424d4168364241674145415526616d703b7573673d414f7656617733505243566f5a5532675a494b44336b62615072544e223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d2272514d516f6420586235565265223e54686520636f756e7472696573206d6f7374206174207269736b206f662061207761746572206372697369733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e3c7370616e20636c6173733d2272514d516f6420614a79694f63223e4178696f733c2f7370616e3e20b72031206461792061676f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c6469763e3c6120687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545617574686f7226616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673513646347743586f4543415951416726616d703b7573673d414f7656617733626475774e6f5934356137397a30464775566e566d223e3c64697620636c6173733d226b43725954223e3c64697620636c6173733d226f754a3943622053374a647a6522207374796c653d2277696474683a343070783b6865696768743a34307078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a343070783b6d61782d6865696768743a34307078222069643d2264696d675f312220646174612d64656665727265643d2231223e3c2f6469763e3c6469763e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f7320262331303030333b3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e54776974746572202623383235303b206178696f733c2f6469763e3c2f6469763e3c64697620636c6173733d22526f434c6e65223e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393838373636383733393733393634382533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c5177436e6f4543415951424126616d703b7573673d414f7656617733765932387a4a6374626a64305a365357505a344c63223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5477697474657220686173207265696e737461746564204d69746368204d63436f6e6e656c6c27732072652d656c656374696f6e2063616d706169676e206163636f756e74206166746572207468652070726f66696c65207761732073757370656e646564206561726c6965722074686973207765656b2e207777772e6178696f732e636f6d2f6d697463682d6d63636f6e2623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e313920686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393838333433333933313336363430322533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517743336f4543415951426726616d703b7573673d414f76566177336c4b50674566354d5351724b75784655584d614f41223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4e657720746563686e6f6c6f677920697320726573686170696e67206173736574206d616e6167656d656e7420666f722061206e65772067656e65726174696f6e206f6620776f726b6572732c20616e64206d6f7374206f662074686520696e64757374727920686173206e6f74206b65707420706163652e207777772e6178696f732e636f6d2f61737365742d6d616e61672623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393837373735323139323939353333332533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517744486f4543415951434126616d703b7573673d414f7656617732554b4158715044627577556a736d37626342675874223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5374657068656e20526f73732022667265616b6564206f75742220617420746865206261636b6c61736820746f20686973205472756d702066756e64726169736572206173205472756d70206173736f636961746573207065727375616465642068696d20746f20676f206168656164207769746820746865206576656e742061742068697320536f757468616d70746f6e206d616e73696f6e2e207777772e6178696f732e636f6d2f7374657068656e2d726f732623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393837323436373432383530373634392533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517744586f4543415951436726616d703b7573673d414f7656617733683854556374412d32397252656e584742497a7a5f223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4265796f6e6420726570616972696e6720616e6420696d70726f76696e6720726f61647320616e64207369646577616c6b732c20636974696573206861766520616e206f70706f7274756e69747920746f206275696c6420696e667261737472756374757265207468617420636f756c64206f70656e207570206e6577206d6f62696c697479206f7074696f6e7320616e6420696e637265617365206163636573736962696c6974792c207772697465732045787065727420566f6963657320636f6e7472696275746f722048656e727920436c6179706f6f6c2e207777772e6178696f732e636f6d2f616d642d6c6973612d73752623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393836373138323634333430343830322533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c5177446e6f4543415951444126616d703b7573673d414f765661773056326641356f4573505f6b723267467962676b5568223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e414d442043454f204c697361205375206469736375737365732077697468204178696f732068657220636f6d70616e79277320726573757267656e636520616e6420626174746c6573207769746820496e74656c207777772e6178696f732e636f6d2f616d642d6c6973612d73752623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323120686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f6769746875622e636f6d2f6178696f732f6178696f7326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4150656751494242414226616d703b7573673d414f7656617733486a474b6775516d6342745456445566344d665072223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e6178696f732f6178696f733a2050726f6d697365206261736564204854545020636c69656e7420666f72207468652062726f7773657220616e64202e2e2e202d204769744875623c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f6769746875622e636f6d202623383235303b206178696f73202623383235303b206178696f733c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e636f6e7374206178696f73203d207265717569726528276178696f7327293b202f2f204d616b652061207265717565737420666f72206120757365722077697468206120676976656e204944206178696f732e2067657428272f757365723f49443d31323334352729202e7468656e2866756e6374696f6e2028726573706f6e736529207b202f2f2068616e646c652073756363657373a02e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517757347745486f4543416b51416726616d703b7573673d414f7656617733565033564769536e46355355474c45653730387037223e3c696d6720636c6173733d2255796b5439642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a373270783b6d61782d6865696768743a37327078222069643d2264696d675f32332220646174612d64656665727265643d2231223e3c2f613e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e4178696f733c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e576562736974653c2f6469763e3c2f7370616e3e3c64697620636c6173733d226e5954375162223e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4178696f7320697320616e20416d65726963616e206e65777320616e6420696e666f726d6174696f6e207765627369746520666f756e64656420696e203230313620627920666f726d657220506f6c697469636f207374616666657273204a696d2056616e64654865692c204d696b6520416c6c656e2c20616e6420526f7920536368776172747a2e204974206f6666696369616c6c79206c61756e6368656420696e20323031372e2054686520736974652773206e616d65206973206261736564206f6e2074686520477265656b3a202623373934303b26233935383b26233935333b26233935393b26233936323b2c206d65616e696e672022776f72746879222e203c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f656e2e77696b6970656469612e6f72672f77696b692f4178696f735f28776562736974652926616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516d684d77456e6f4543416b51427726616d703b7573673d414f76566177306a76706f32364a65425a684a7638496157384e7163223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e57696b6970656469613c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c64697620636c6173733d22766253684f65206b43725954223e3c64697620636c6173733d22415673657066223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e3e3c7370616e20636c6173733d22424e656177652073337639726420415037576e64223e4f776e65723c2f7370616e3e3c2f7370616e3e3a203c7370616e3e3c7370616e20636c6173733d22424e6561776520744164384420415037576e64223e4178696f73204d6564696120496e633c2f7370616e3e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d2241567365706620753278314f64223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e3e3c7370616e20636c6173733d22424e656177652073337639726420415037576e64223e4c61756e636865643c2f7370616e3e3c2f7370616e3e3a203c7370616e3e3c7370616e20636c6173733d22424e6561776520744164384420415037576e64223e323031373c2f7370616e3e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e50656f706c6520616c736f2073656172636820666f723c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22787063223e3c64697620636c6173733d22783534677466223e3c2f6469763e3c6469763e3c64697620636c6173733d226b43725954223e3c7370616e20636c6173733d2270756e657a223e3c64697620636c6173733d22424e656177652077797277586320415037576e64223e4c69626572616c206e65777320736f7572636573206c6973743c2f6469763e3c2f7370616e3e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d434e4e26616d703b737469636b3d483473494141414141414141414f4f5155654c517a3955335343394f4c7a4b537a4d6c4d5369314b7a4648495379307656696a4f4c79314b54693157794d6b734c6f6c6954617a497a43382d7851685866497152433851304d6f38334b726545636a4a79697772536b36434b6b67324b633644693859565a35686e707678676c6658435a333844437549695632646e504477435833376e476b414141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454149223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f332220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e434e4e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d506f6c697469636f26616d703b737469636b3d483473494141414141414141414f4f5155654c537a3955334d444b504e7971334e4a4c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794146536e6c3663586e534b45556b6e6c4a4f52573153516e6752566c4778516e414d566a795f4d4d7339495f38556f3659504c5f415957786b5773484148354f5a6b6c6d636e3541484749687547584141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945414d223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f352220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e506f6c697469636f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d48756666506f737426616d703b737469636b3d483473494141414141414141414f4f5155654c537a395533794d67744b6b68504d704c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794146536e6c3663586e534b45617a5479447a65714e7753796f45594131575562464363417857504c3877797a306a5f78536a7067387638426862475261776348715670615148357853554175376a463070634141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454151223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f372220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e48756666506f73743c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d4e505226616d703b737469636b3d483473494141414141414141414f4f5155654c517a3955335344596f7a6a47537a4d6c4d5369314b7a4648495379307656696a4f4c79314b54693157794d6b734c6f6c6954617a497a43382d785168576e463663586e534b6b5176454e444b504e79713368484979636f734b30704f67696b416d5173586a4337504d4d394a5f4d55723634444b5f675956784553757a583041514142514b584c32514141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454155223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f392220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4e6174696f6e616c205075626c69632052612e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d226d4868796c66223e3c64697620636c6173733d22575a35474a66223e3c6120636c6173733d22714e394b65642220687265663d222f7365617263683f69653d5554462d3826616d703b713d4c69626572616c2b6e6577732b736f75726365732b6c69737426616d703b737469636b3d483473494141414141414141414f4f514d5a4c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794b476671322d515870786564497152433851304d6f38334b726545636a4a79697772536b36434b6b67324b633644693859565a35686e707678676c6658435a3338444375496756747a514146695a305070774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517a4f30424d425a3642416749454159223e3c627574746f6e20636c6173733d2244586b354d65205169394664223e3c696d6720636c6173733d22685748754a2220616c743d224172726f7722207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a323470783b6d61782d6865696768743a32347078222069643d2264696d675f31312220646174612d64656665727265643d2231223e3c2f627574746f6e3e3c64697620636c6173733d22424e65617765206a69356a706620744164384420415037576e64223e4d6f726520726573756c74733c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c64697620636c6173733d22687763223e3c64697620636c6173733d226b43725954223e3c7370616e20636c6173733d2270756e657a223e3c64697620636c6173733d22424e656177652077797277586320415037576e64223e4c69626572616c20706f6c69746963616c2077656273697465733c2f6469763e3c2f7370616e3e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d536c6174652b4d6167617a696e6526616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d4449724e7259776b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a5762787875565730493547626c4642656c4a70786752426b456c7a4d334b636f70796f524c47795a55705656433253564a4b5354715562576c55626c7a796931484b423666564453794d69316a35676e4d5353314956664250544536737938314942452d4b766d72674141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454167223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31332220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e536c617465204d6167617a696e653c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d42757a7a4665656426616d703b737469636b3d483473494141414141414141414f4f5155654c537a3955334d44637279796e4b4e5a4c4b7955784b4c55724d55536a497a386b7379557747737370546b346f7a53314b4c6f3167544b7a4c7a69303878677455626d6363626c5674434f526d3552515870536163594f6345795a735847466c414a694b6c514365506b797051714b4e736b4b61556b486371324e436f334c766e464b4f5744302d6f4746735a467242784f7056565662716d704b5144564e44327173774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945416b223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31352220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e42757a7a466565643c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d4461696c792b4b6f7326616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d453675544b6b796b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a51626d6363626c5674434f526d3552515870536163597751595a6d5255625730416c7a4d334b636f70796f524a67473642736b3653556b6e516f32394b6f334c6a6b46364f554430367247316759463746797569526d356c5171654f635841774447505a396673774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945416f223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31372220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4461696c79204b6f733c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d54616c6b696e672b506f696e74732b4d656d6f26616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d456c4b4b556b336b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a51626d6363626c5674434f526d3552515870536163597751595a6d5255625730416c7a4d334b636f70796f524c47795a5570565641323244596f32394b6f334c6a6b46364f5544303672473167594637454b6879546d5a47666d705373453547666d6c5251722d4b626d35674d415637446f6d62304141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454173223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31392220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e54616c6b696e6720506f696e7473204d652e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d226d4868796c66223e3c64697620636c6173733d22575a35474a66223e3c6120636c6173733d22714e394b65642220687265663d222f7365617263683f69653d5554462d3826616d703b713d4c69626572616c2b706f6c69746963616c2b776562736974657326616d703b737469636b3d483473494141414141414141414f4f514d5a4c4b7955784b4c55724d55536a497a386b7379557747737370546b346f7a53314b4c6f3167544b7a4c7a6930387863756e6e366873596d6363626c5674434f526d3552515870536163594f6345795a735847466c414a63374f796e4b4a6371495278636d564b465a52746b7052536b67356c57787156473566385970547977576c314177766a496c5938386744456f63564e75514141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517a4f30424d425a3642416749454177223e3c627574746f6e20636c6173733d2244586b354d65205169394664223e3c696d6720636c6173733d22685748754a2220616c743d224172726f7722207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a323470783b6d61782d6865696768743a32347078222069643d2264696d675f32312220646174612d64656665727265643d2231223e3c2f627574746f6e3e3c64697620636c6173733d22424e65617765206a69356a706620744164384420415037576e64223e4d6f726520726573756c74733c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c68723e3c64697620636c6173733d226475662d68223e3c64697620636c6173733d22664c74587363206949576d346222206f6e636c69636b3d227870287468697329223e3c64697620636c6173733d224c796d3857223e3c64697620636c6173733d2241655151756220687763223e3c2f6469763e3c64697620636c6173733d2259435537656220687763223e3c2f6469763e3c64697620636c6173733d2249795961456420687778223e3c2f6469763e3c64697620636c6173733d2245435548516520687778223e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f656e2e77696b6970656469612e6f72672f77696b692f4178696f735f28776562736974652926616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4159656751494252414226616d703b7573673d414f76566177333867573437455f39394a326a683746384149573439223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f7320287765627369746529202d2057696b6970656469613c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f656e2e77696b6970656469612e6f7267202623383235303b2077696b69202623383235303b204178696f735f2877656273697465293c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4178696f7320287374796c697a6564206173204158494f532920697320616e20416d65726963616e206e65777320616e6420696e666f726d6174696f6e207765627369746520666f756e64656420696e203230313620627920666f726d657220506f6c697469636f207374616666657273204a696d2056616e64654865692c204d696b6520416c6c656e2c20616e6420526f7920536368776172747a2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e66616365626f6f6b2e636f6d2f6178696f736e6577732f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a415a656751494178414226616d703b7573673d414f76566177306e6d4a4834444273432d317679714158357832346a223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f73202d20486f6d65207c2046616365626f6f6b3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e66616365626f6f6b2e636f6d202623383235303b202e2e2e202623383235303b204272616e64202623383235303b2057656273697465202623383235303b204e6577732026616d703b204d6564696120576562736974653c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5468657265206973206e6f207761792052657075626c6963616e732063616e206368616e6765206269727468207261746573206f7220637572622074686973207472656e64202623383231323b20616e642074686572652773206e6f7420612073696e676c652064656d6f67726170686963206d6567617472656e642074686174206661766f72732052657075626c6963616e732e206178696f732e636f6d2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e68626f2e636f6d2f6178696f7326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4161656751494168414226616d703b7573673d414f7656617730314959735a763273624653415f745778396f6a6850223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f73202d204f6666696369616c205765627369746520666f72207468652048424f20536572696573202d2048424f2e636f6d3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e68626f2e636f6d202623383235303b206178696f733c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4b6e6f776e20666f722064656c69766572696e67206e6577732c20636f7665726167652c20616e6420696e7369676874207769746820612064697374696e6374697665206272616e64206f6620736d61727420627265766974792c204178696f73206f6e2048424f2068656c707320766965776572732062657474657220756e6465727374616e642074686520626967207472656e6473a02e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e52656c617465642073656172636865733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6a7326616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454145223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73206a733c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6a73223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b68626f26616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454149223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732068626f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b68626f223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6d65616e696e6726616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a364241674245414d223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73206d65616e696e673c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6d65616e696e67223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76756526616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454151223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73207675653c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b767565223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76732b666574636826616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454155223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732076732066657463683c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76732b6665746368223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b72656163742b6e617469766526616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454159223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73207265616374206e61746976653c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b72656163742b6e6174697665223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b74762b73686f7726616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454163223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732074762073686f773c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b74762b73686f77223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b63646e26616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454167223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732063646e3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b63646e223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c666f6f7465723e203c6469763e20203c64697620636c6173733d225a494e62626320787064204f396735636320755550476920426d50357466223e3c64697620636c6173733d226e4d796d6566204d5578476264206c794c776c63223e3c6120636c6173733d226e424445316220473565466c662220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b65693d704d524f586250624472505339414f426d4b5059425126616d703b73746172743d313026616d703b73613d4e2220617269612d6c6162656c3d224e6578742070616765223e4e657874202667743b3c2f613e3c2f6469763e3c2f6469763e203c2f6469763e2020203c6469762069643d226d436c6a6f62223e3c6469763e3c6120687265663d222f75726c3f713d68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d2f536572766963654c6f67696e253346636f6e74696e7565253344687474703a2f2f7777772e676f6f676c652e636f6d2f73656172636825323533467125323533446178696f7325323532366f7125323533446178696f73253235323661717325323533446368726f6d652e302e36396935396c326a306c336a36396936302e3831316a306a372532353236736f75726365696425323533446368726f6d652532353236696525323533445554462d38253236686c253344656e26616d703b73613d5526616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517873384343475126616d703b7573673d414f765661773338654a695f306b32366743644b726d315057417979223e5369676e20696e3c2f613e3c2f6469763e3c6469763e3c6120636c6173733d226b73545534632220687265663d22687474703a2f2f7777772e676f6f676c652e636f6d2f707265666572656e6365733f686c3d656e2d434126616d703b66673d3126616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735135665543434755223e53657474696e67733c2f613e3c6120636c6173733d226b73545534632220687265663d222f2f7777772e676f6f676c652e636f6d2f696e746c2f656e5f63612f706f6c69636965732f707269766163792f3f66673d31223e507269766163793c2f613e3c6120636c6173733d226b73545534632220687265663d222f2f7777772e676f6f676c652e636f6d2f696e746c2f656e5f63612f706f6c69636965732f7465726d732f3f66673d31223e5465726d733c2f613e3c2f6469763e3c2f6469763e20203c2f666f6f7465723e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220686c3d27656e2d4341273b2866756e6374696f6e28297b76617220623d746869737c7c73656c662c643d2f5e5b5c772b2f5f2d5d2b5b3d5d7b302c327d242f2c653d6e756c6c3b76617220663d646f63756d656e742e717565727953656c6563746f7228222e6c22292c673d646f63756d656e742e717565727953656c6563746f72282223736622292c6b3d672e717565727953656c6563746f7228222e73626322292c6c3d672e717565727953656c6563746f7228225b747970653d746578745d22292c6d3d672e717565727953656c6563746f7228225b747970653d7375626d69745d22292c6e3d672e717565727953656c6563746f7228222e736322292c703d672e717565727953656c6563746f7228222e7822292c713d6c2e76616c75652c723d5b5d2c743d2d312c753d712c772c782c793b717c7c2870262628702e7374796c652e646973706c61793d226e6f6e6522292c7a28213129293b66756e6374696f6e207a2861297b6966286b2e636c6173734c6973742e636f6e7461696e732822657362632229297b76617220633d6b2e636c6173734c6973742e636f6e7461696e732822636873626322292c683d6b2e636c6173734c6973742e636f6e7461696e73282272746c73626322293b612626286e2e7374796c652e646973706c61793d22626c6f636b222c633f28672e7374796c652e626f726465725261646975733d2232307078203230707820302030222c6e2e7374796c652e626f72646572426f74746f6d3d2231707820736f6c69642023444645314535222c6d2e7374796c652e626f726465725261646975733d683f2232307078203020302030223a223020323070782030203022293a6b2e7374796c652e626f726465725261646975733d683f22302038707820302030223a2238707820302030203022293b617c7c286e2e7374796c652e646973706c61793d226e6f6e65222c633f28672e7374796c652e626f726465725261646975733d2232307078222c6e2e7374796c652e626f72646572426f74746f6d3d226e6f6e65222c6d2e7374796c652e626f726465725261646975733d683f2232307078203020302032307078223a223020323070782032307078203022293a6b2e7374796c652e626f726465725261646975733d683f223020387078203870782030223a22387078203020302038707822297d7d66756e6374696f6e204128297b672e717565727953656c6563746f7228225b6e616d653d6f715d22292e76616c75653d753b672e717565727953656c6563746f7228225b6e616d653d6171735d22292e76616c75653d22686569726c6f6f6d2d7372702e222b28303c3d743f743a2222292b222e222b28303c722e6c656e6774683f22306c222b722e6c656e6774683a2222297d0a66756e6374696f6e204328297b773d6e756c6c3b69662878297b76617220613d222f636f6d706c6574652f7365617263683f636c69656e743d686569726c6f6f6d2d73727026686c3d222b686c2b22266a736f6e3d742663616c6c6261636b3d685326713d222b656e636f6465555249436f6d706f6e656e742878293b22756e646566696e656422213d3d747970656f6620647326266473262628612b3d222664733d222b6473293b76617220633d646f63756d656e742e637265617465456c656d656e74282273637269707422293b632e7372633d613b6966286e756c6c3d3d3d6529613a7b613d622e646f63756d656e743b69662828613d612e717565727953656c6563746f722626612e717565727953656c6563746f7228227363726970745b6e6f6e63655d222929262628613d612e6e6f6e63657c7c612e67657441747472696275746528226e6f6e63652229292626642e74657374286129297b653d613b627265616b20617d653d22227d28613d65292626632e73657441747472696275746528226e6f6e6365222c61293b646f63756d656e742e626f64792e617070656e644368696c642863293b783d6e756c6c3b773d73657454696d656f757428432c353030297d7d0a66756e6374696f6e204428297b666f72283b6e2e66697273744368696c643b296e2e72656d6f76654368696c64286e2e66697273744368696c64293b723d5b5d3b743d2d313b7a282131297d66756e6374696f6e204528297b76617220613d6e2e717565727953656c6563746f7228222e73637322293b61262628612e636c6173734e616d653d2222293b303c3d743f28613d6e2e6368696c644e6f6465735b745d2c612e636c6173734e616d653d22736373222c713d612e74657874436f6e74656e74293a713d753b6c2e76616c75653d717d6c2e6164644576656e744c697374656e65722822666f637573222c66756e6374696f6e28297b66262628662e7374796c652e646973706c61793d226e6f6e6522297d2c2131293b6c2e6164644576656e744c697374656e65722822626c7572222c66756e6374696f6e28297b4428293b66262628662e7374796c652e646973706c61793d2222297d2c2131293b6c2e6164644576656e744c697374656e657228226b65797570222c66756e6374696f6e2861297b713d6c2e76616c75653b793d21313b31333d3d612e77686963683f4128293a32373d3d612e77686963683f284428292c66262628662e7374796c652e646973706c61793d2222292c713d752c6c2e76616c75653d71293a34303d3d612e77686963683f28742b2b2c743e3d722e6c656e677468262628743d2d31292c452829293a33383d3d612e77686963683f28742d2d2c2d313e74262628743d722e6c656e6774682d31292c452829293a28613d71293f2870262628702e7374796c652e646973706c61793d2222292c783d612c777c7c4328292c753d61293a2870262628702e7374796c652e646973706c61793d226e6f6e6522292c7a282131292c4428292c753d22222c793d2130297d2c2131293b6d2e6164644576656e744c697374656e65722822636c69636b222c412c2131293b702e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e28297b6c2e76616c75653d22223b702e7374796c652e646973706c61793d226e6f6e65223b7a282131297d2c2131293b6b2e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e28297b6c2e666f63757328297d2c2131293b77696e646f772e68533d66756e6374696f6e2861297b6966282179297b4428293b303d3d615b315d2e6c656e67746826267a282131293b666f722876617220633d303b633c615b315d2e6c656e6774683b632b2b297b76617220683d615b315d5b635d5b305d2c763d646f63756d656e742e637265617465456c656d656e74282264697622293b762e696e6e657248544d4c3d683b762e6164644576656e744c697374656e657228226d6f757365646f776e222c66756e6374696f6e2842297b422e70726576656e7444656661756c7428293b72657475726e21317d2c2131293b683d682e7265706c616365282f3c5c2f3f623e2f672c2222293b762e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e2842297b72657475726e2066756e6374696f6e28297b743d423b4128293b4528293b4428293b672e7375626d697428297d7d2863292c2131293b6e2e617070656e644368696c642876293b7a282130293b722e707573682868297d7d7d3b7d292e63616c6c2874686973293b7d2928293b2866756e6374696f6e28297b66756e6374696f6e20622861297b666f7228613d612e7461726765747c7c612e737263456c656d656e743b612626224122213d612e6e6f64654e616d653b29613d612e706172656e74456c656d656e743b61262628612e687265667c7c2222292e6d61746368282f5c2f7365617263682e2a5b3f265d74626d3d697363682f29262628612e687265662b3d22266269773d222b646f63756d656e742e646f63756d656e74456c656d656e742e636c69656e7457696474682c612e687265662b3d22266269683d222b646f63756d656e742e646f63756d656e74456c656d656e742e636c69656e74486569676874297d646f63756d656e742e6164644576656e744c697374656e65722822636c69636b222c622c2131293b646f63756d656e742e6164644576656e744c697374656e65722822746f7563685374617274222c622c2131293b7d292e63616c6c2874686973293b3c2f7363726970743e3c2f6469763e3c212d2d206363746c636d2035206363746c636d202d2d3e3c746578746172656120636c6173733d2263736922206e616d653d2263736922207374796c653d22646973706c61793a6e6f6e65223e3c2f74657874617265613e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220653d27704d524f586250624472505339414f426d4b50594251273b76617220736e3d27776562273b2866756e6374696f6e28297b76617220713d66756e6374696f6e2861297b76617220633d77696e646f772c643d646f63756d656e743b69662821617c7c226e6f6e65223d3d612e7374796c652e646973706c61792972657475726e20303b696628642e64656661756c74566965772626642e64656661756c74566965772e676574436f6d70757465645374796c65297b76617220623d642e64656661756c74566965772e676574436f6d70757465645374796c652861293b696628622626282268696464656e223d3d622e7669736962696c6974797c7c22307078223d3d622e686569676874262622307078223d3d622e7769647468292972657475726e20307d69662821612e676574426f756e64696e67436c69656e74526563742972657475726e20313b76617220663d612e676574426f756e64696e67436c69656e745265637428293b613d662e6c6566742b632e70616765584f66667365743b623d662e746f702b632e70616765594f66667365743b766172206b3d662e77696474683b663d662e6865696768743b72657475726e20303e3d662626303e3d6b3f303a303e622b663f323a623e3d28632e696e6e65724865696768747c7c642e646f63756d656e74456c656d656e742e636c69656e74486569676874293f333a303e612b6b7c7c613e3d28632e696e6e657257696474687c7c642e646f63756d656e74456c656d656e742e636c69656e745769647468293f343a317d3b76617220723d652c763d736e3b66756e6374696f6e207728612c632c64297b613d222f67656e5f3230343f617479703d63736926733d222b28767c7c2277656222292b2226743d222b612b2822266c6974653d312665693d222b722b2226636f6e6e3d222b2877696e646f772e6e6176696761746f72262677696e646f772e6e6176696761746f722e636f6e6e656374696f6e3f77696e646f772e6e6176696761746f722e636f6e6e656374696f6e2e747970653a2d31292b63293b633d222672743d223b666f7228766172206220696e206429612b3d632b622b222e222b645b625d2c633d222c223b72657475726e20617d66756e6374696f6e20782861297b613d7b7072743a617d3b77696e646f772e77737274262628612e777372743d77696e646f772e77737274293b72657475726e20617d66756e6374696f6e20792861297b77696e646f772e70696e673f77696e646f772e70696e672861293a286e657720496d616765292e7372633d617d0a2866756e6374696f6e28297b666f722876617220613d2b6e657720446174652d77696e646f772e73746172742c633d782861292c643d302c623d302c663d302c6b3d646f63756d656e742e676574456c656d656e747342795461674e616d652822696d6722292c6d3d2226696d6e3d222b6b2e6c656e6774682b22266269773d222b77696e646f772e696e6e657257696474682b22266269683d222b77696e646f772e696e6e65724865696768742c413d66756e6374696f6e28682c7a297b682e6f6e6c6f61643d66756e6374696f6e28297b623d2b6e657720446174652d77696e646f772e73746172743b7a26262b2b6e3d3d66262628643d622c742829293b682e6f6e6c6f61643d6e756c6c7d7d2c743d66756e6374696f6e28297b6d2b3d2226696d613d222b663b632e6166743d643b7928772822616674222c6d2c6329297d2c6e3d302c423d302c673d766f696420303b673d6b5b422b2b5d3b297b766172206c3d313d3d712867293b6c26262b2b663b76617220753d672e636f6d706c657465262621672e6765744174747269627574652822646174612d646566657272656422292c703d7526264e756d62657228672e6765744174747269627574652822646174612d696d6c2229297c7c303b752626703f286c26262b2b6e2c70262628673d702d77696e646f772e73746172742c6c262628643d4d6174682e6d617828642c6729292c623d4d6174682e6d617828622c672929293a4128672c6c297d647c7c28643d61293b627c7c28623d64293b6e3d3d6626267428293b77696e646f772e6164644576656e744c697374656e657228226c6f6164222c66756e6374696f6e28297b77696e646f772e73657454696d656f75742866756e6374696f6e28297b632e6f6c3d2b6e657720446174652d77696e646f772e73746172743b632e696d6c3d623b76617220683d77696e646f772e706572666f726d616e6365262677696e646f772e706572666f726d616e63652e74696d696e673b68262628632e727173743d682e726573706f6e7365456e642d682e7265717565737453746172742c632e727370743d682e726573706f6e7365456e642d682e726573706f6e73655374617274293b7928772822616c6c222c6d2c6329297d2c30297d2c2131297d2928293b7d292e63616c6c2874686973293b7d2928293b3c2f7363726970743e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e66756e6374696f6e205f736574496d6167657353726328652c63297b66756e6374696f6e20662861297b612e6f6e6572726f723d66756e6374696f6e28297b612e7374796c652e646973706c61793d226e6f6e65227d3b612e7372633d637d666f722876617220673d302c623d766f696420303b623d655b672b2b5d3b297b76617220643d646f63756d656e742e676574456c656d656e74427949642862293b643f662864293a2877696e646f772e676f6f676c653d77696e646f772e676f6f676c657c7c7b7d2c676f6f676c652e6969723d676f6f676c652e6969727c7c7b7d2c676f6f676c652e6969725b625d3d63297d7d3b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f6a7065673b6261736536342c2f396a2f34414151536b5a4a5267414241514141415141424141442f3277434541416b474277674842676b494277674b43676b4c445259504451774d445273554652415749423069496941644878386b4b4451734a4359784a7838664c5430744d5455334f6a6f364979732f52443834517a51354f6a634243676f4b4451774e47673850476a636c487955334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e2f2f4141424549414367414b414d4249674143455145444551482f7841415a4141414441514542414141414141414141414141414141414267634243414c2f784141734541414241774d434241554541774141414141414141414241674d454141555242694548456a4642453147426b61456959634852464256782f3851414751454141674d424141414141414141414141414141414141514944424155412f385141485245414177454141674d424141414141414141414141414141454341784a4242424d7842662f61414177444151414345514d5241443841754e655843416e4a3644725735706631746450362b794f6870584b2b2b5044622b3265703971664f48644b56325236364c4b4862364632343858744c57366139456b6d64346a4b696c5254487944397763394b66597237556d4d3149595746744f6f43304c4232556b6a4949394b3566313559333151303374686e4d6474774d504c386c455a542b765565645666675271495858536f746a7a67564a7468434d5a33384d377039747836553232667274794a342b767479566c506f7242306f71496e4d5053705a7275354766656a486279576f333044486452362f67565272784a6369323139356c70546a6752394345444a4a376256504e4c574b5a4c76726239776a504962625558564b6451527a4b37664e58764334787931726f797630656438635a58305a4470526d586f682b7879414f61537965645248527737672b687837564265476c346530667841615a6d6b7474716456436d4950626647663841516f44357271554441785541343161496e6a564b62725a59456951334f547a752f7741644255554f4a786b37644d6a487a564f716455365a705a776f68536a6f45644b4b576548647a6e584c53634a64316a5078357a4b664265513867704a4b647562667a4739464b4f4d7441414859555555414734464742525252434742355555555678782f2f32515c7833645c783364273b76617220693d5b2764696d675f31275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414945414141424943414d414141446d6d575965414141416f6c424d5645556949694c2f2f2f38414141415a47526b644852337537753751304e41534568494b6d654156465256675947414f4467352b666e3757317462382f507a643364314c533073784d5447506a342b656e703675727134714b69705a57566d30744c544477384e766232382f507a2f303950546a342b4f38764c783364336547686f596a484255556572416545414137676138416d657133312f496a46674d586135704570654d416b64375336506f61585952666b376f4f6a63346453474d476e2b6f564141416a445141664f6b77684c6a636b4141416256485a4d6b39507941414143636b6c455156526f6765325732334c694d41794748646b4a635135415349434568435464412b774a324c624c2b372f61536e4b67334f4f5a6e646e526431484c78575039746e384a6c42494551524145515241455152442b41347a57326a794d48476a3661397a6e4d3044306662314e30356e784b7142496b6d53706c55356f744e4d2f6d7655475a355149564c614e366e49444c6d74716d374c4d647542527761665051524345566b474634787a504452454743326944494d633855474a414c48597036656e647450497034637458334447624b656877334550615577616c777943495145456533476c5342634e747376636d3458434d762b47474853697a6f353033674b6d4441517772674a7254686537675331764d6359676958756a4c43324d63662f394247326f464b787a7a4d7544444f77554e35616f4c6f2f6f35793654374b5145324e4b536542507938785045766c395135674e78677a4b5341336d434c486a5441463751636e414b6f4275767246635a54484d66734144532b556650626730384b5345374231773137537235685439617235554e3150696e6766496b764c37387a50697157576e4d33475374594f3463517475645046704d5238384b54684665386773766261454e33566e594350346854554842644f67556b726f5a37626379564879652b6f51744f72316a30624331584456514a44362f51756c54634c6b704949616c797168612b732b6435666345724f492f4b724e45414c62416670764d354252326e785a57574c5a4c73686d67414d4d6e48367a7a4a4f39727739416344324f4b6550643345766e2b73526d35417051565930726c4436685768776c7277706542775242396552347a342f6b4f36434e634673395170534e6c355965344d6d5042547446585a6565764c65415778693236746f48646d6d4538393052547452316465676448686255493934326d7747313075787750486d6973647a613563515554544e354f355351766168507870706e4c73646a3671635478667239663361574c364c4d75474e5956416b634a3551316c67557932365254335947532f4457716a7262652b704934334966574a6e6941733577726e4c6f6748774e387273746b3754377858724a623867434949674349496743494c77442f6b4c6b327368546d377038716741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3233275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416231424d5645584d4141442f2f2f2f4b4141444c4141442b2b2f763435655879304e44514e7a666f72713754516b4c72744c54696b5a484d4a53584b44772f34346548484141446f7036663939766235367572383866483031396676784d54676834664f49694c61626d37636433666b6d4a6a78793876575631666e6f4b445453456a55546b3759596d4c4e4768726666332f514d444475766231676d3047614141414758306c455156526f6762576234614b714b68434662636a4b4c44484e637165563758722f5a37774774484d424b70347238394f5154324341575441464732584868635757595855706e337661373571345341363249734e325349713432584861503875344374385642467a5a31564a36382f50675243774941736149732f4b6154634e6c61636d346546395538506a5a484261424d6b714e316d31755442622b4b30507265446d68645a63316f2b373762585852706864346a446a51354376385562676969783033336d387236414e7548695a4f764c452f4f65475746374a5830414d383952656e327346356c67333176573844486b365733756751387a46653367785659414b76773539487a566a37546d507451324131396e6e384d6779386a725950674e6c75654c686265367947654b45447277754d4866716a48484b636376534441626a614f78546e6d333765316f585841526f443369354b6e477654684c4665337145304b6e67766e58385039394c3442376a55696a4d57586174566358706f79317a762f452f3043756965726c61766e346436486b704c506e4e7269794e4f74304a3953486f4734726e50627a51586f4470524c592f6c34715758787736682b727442464d38756b637165525256396e48362b377255395734444c573763384f3364724c634364714c44796374776736753576477a4b4279647053616162325876416e74724d3273514967443858444d4a466c617a4c47734470333637794c702b6b39716f2f79363646544c58753261735633524d537a2b4261565969535037592f616a722f64473156754f574e734a346862614f4c5446705a63756b5834567644614375695734612b6665516856306e74364c392f393041372b7531507942707234612b6c5563464b7133674d6933494c452b68762b6a64676645476f5577456738496a454e30472b3470596b4958416d497245414f6a304d4c4a5a44747865386c64756f556f4278505a3241673136495170366b5265765544412f3661426d5272345767704e44454b3359464d5241727577486235464835547779676253326f2f55506252424744417a4b6b52554f494f4446672b445569524b464a446b667345494a585467476f65342f354457336467384b696d41646b394d2b70557a39794172446c4d416759557638746b734b507732423359397047786c6734436c593973594a506d6f5475513751496c44394f6a47314175463276636974324241656e37345168512b6367524e3732584f394459674d654163726e4157423745787478413556304a646d7271455269516d41596f48336a75452f676a436d4b3038657352474f78465a495842307237774347534e384a73664b4e6b632f41454445744e673159337631444c72436368494e43666d38477a704436693233514d63644644734561695544473746753556486f4654417141537057666f444b744652595a5261654151714266794c3673596a554a3263354f673346342f4134476d4a55742f507641476c6b74485554656b524b4a58526f734374654f5552794e6269425467325a48655051435658387a31757852642f514b566b674d42714f506561462f694a55753851382b424a78377a41674552732b656f2f444a77644b4b505565793978426941396f454b706276712f3650384464316563427161366d526e347a4c4842596d7145634877316377767871456b706d5733506f664d73774d5664587a34586d72715a47336a456f4e73537063344d314134564c65706d6271424e795352506a304474356b5a47716462626e4c6d416d674b5736755a70385a765a674158347a554e737862596c6454626745734931715752513363774d58427a4252355477683256325a714332664d717a31462b6a69544d434678685a53415673584f504f436453556a4e694b4e336f5435775271536b61637752386172596c7a416e55464c4657785436436d5a4f5235757a5931484941702b4d4967634d6b4d4a625049344a6b445542755a51534436694f304d33674749303273456d454f594c34562f6a74757a425a68324c483571517a414d314f3970684d6948726467473542335438794847674f676a37436165645a74744177375a4b44416a773866674c4856326f4f59335574313074754c3567526e653030683138333168667544696851366852616b65674b69413154334e3766504d4278447661526775715436416d674a4764654d46695063303671723336524734534347456b3165393667782b4b6c43635347493269415834395248426b506c4c612f6f486f4c7a4777767966737757493236343862792f454d3345496b4a783731314c4e784257734a6c335746694157495855477a786e6a49764249554479477653595073554f494a5042693632505961536f314a3637726b336c6b79347a304673304f6d4f4245396e77767a572f6b6c554b75726d74776f6f34416c31712b45612f7335664463464c6f427732506a4f67397871316f54373953547449634b5746374179797069644645654a4c3157706555443239664b335a37304d6b334a554b507550784d74556d58314a776e4c596b5941454e6853495a536875676b5933654c5836334c5455305a356f566335624e594d476d6b587266506679576e4746374d6f6d38537a547772567158754839615231386d6c4150512b696177574e767339752b5351672f78336757525777586b47786d414a5561553239466f356c62516f56366335546556674474724b6b4d5864355a5434467950686754716d777a5a446a55435136794a56483535354644657a566b7a76397275416d46796e48356c467035434c5a652f566d397878474a3756477565484f71577565666e62694669522f76443672367a694d7a6c45364957462b6357783257696f7165386266377833613856746a55586c7847547845786e64362f366d674e5349656c476c334f4437704c5262626271746a6b76334466783765365a7176754978327a3374357262522f54667748444f747262744e7643415541414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f33275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416456424d5645584e4742332f2f2f2f4a4141444d414144373850444d44685856556c4c4d4642726e73612f37382f505556316a76794d62504953583032747276797372464141446a6b35585150554c4d42512f787a39444d41416a36362b765351304c2b2b76726e724b7a32354f5471754c6661654866585a4754737672375a6348445961576e6669597664666f48504d54446a6f364854536b765350446e6a6d70765a704856754141414461306c455156526f67653258323361714d42424177345159735642705141533543496a2b2f79636553494a415346307439727a4e6672484e5a546245535449536769414967694149676941496769414973676b6d597141514338614e446a3567616541324c4d4f48475749646e43625876413772706a6f5145504f78784f395a7a423461664b342b5448547a776769304c664a4742743852594c6f314f6a6b4b4c3768387a5a5277433370536d466169692f71474c49597957484d5a65353947547545615a57636433413175766c59366337496948683845396b5044783079343834615747414a6e545342554c7832466774646e5938776c595376685949692f46626f7668653563534c6c6c56456774517363723645736832495673496153352b58707a59526e6d6564366b6d57342b4a2f53566b454a6d695a5574684841596661653036594f48355578344261423938736230324f37564d4f386758676e4a555350484a7570766d43397058476a6635533644437771515a3072495366794d4b4b4251442b2f434b2b4759467141656a7131377756566871696e704f6656444b61526b546e794d354e413966555049495657724449766748496f6d4a72526343416e7a737a484d5a69487235462b6e75316a47376e644b502b70734e677048657a594c395174326c4b7942306c6b3133527731636276514d535a4f634f7173684379525231314e7477726a5175367473583042726464434168394b7446564935596f477134746a6a4c312b696c784f3844634c5a614b6e31685674413475513357574f4a5275467a4a636e5832564c476461644c4549563646787346616f63574f304a4f6162772f6c346f4f696b6b7470775277354833763453326e486b6c394137764c656d5864556b50746955563356386b5457354e6d7353574e46444c6262483561465062596d2f64467235745736674a356561545268326c67572f37456d6c704f646f365479334a357150744d4877366865314c48413456737731554e66444f625347664f4c4f7436544448664962453053665464754644686d6a57526e6c314c5a766952463265795273584d4c2f4c532f78636d5562654d706134792f654c56526e2b654b6645494e4449494e357559525351317054514734466e4f674856785849774c364b456876326969464a35344e7734544c70447049756f422b744c4f646f586370422f716e48756e5432466e2b32587068552f466e4b69612b5567684b4d4d666b777535366b517a737048474b625065746f564d714f5663474950507859534e6c586e575271476a314c566a665a5350306a55446a4b46743138492b2b71764e4f4e2b49335276342b2b3474345345482b755459394c454b3245354658656d4d41586a42356b7058505953656a63435a4a5536376937425365616d6d333355414e4f39435858304f534f7149536d48542f45554476316c4e77714e3371485568766f6a6b2b2f746e614a39432f72484b764375714b7271756d75424c6737634958466e304434564271597838742f6e453571394b735239642b324446776d647651706e61702f5a6a7665335563475a72654241454152424541524245415242454154354466384152776f2b4948597561487741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f35275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414555414141424643414d414141417255397362414141415946424d564555414141442f2f2f2b4b696f724f7a733553556c4c613274716e70366672362b7648783866382f507a7a382f4e61576c712b7672342b506a373239765a7763484376723639396658316b5a47524d544579676f4b41774d44415044772b31746257616d706f6749434156465255714b6971546b354e455245527061576b354f546d4d703246774141414239456c455156525968613257366136434d4243465757565241554655334f3737762b55464539716d6338704d577336764575424c4d3265324b4e354455534c53536631775171386a6d5470467959522f494f574b4d7670446e73554b4b57372b6c4c4f3653756f506958704675516451486f7079436143552b317255424667304e53736c44376a4b50686131697649496f4f6a3862774d6f2b31696b382f2f7044376d70713452594e4f6a6d456b424a464157336f3965677a3333716b67704c2f4957554d74626e613879712b6f4f55334b436b504b57476b4c6b2b39455042553742466333336f454e55383551417072554535386843485258654430764951522f366e4275584151796f4969577144346d39526246427946754c492f3936676a41304c69612b51636a556f47513978744b695451526d366736334b707277677054446a516a5353752b447633473857585777497a7639736d2f4b774b5868456a2b30734e3657304b572f3374323652477666702f792b5351443558365730497a6e3947695530706653696454656c384b4b51366662616f4737486f374548356b43727947644666472b49316f742b37574554794837636f527353696e76306c79596c4963486d4c79505770696f6d6c43495972586c476e2f766a54373447484f4b624932672b587336542f347847394474526663415555624e487170456e636b474f4c576f764e4a47366f674a447a6d672f4c67324334596f745550357a5049386b774b707a2f4b71435262502f424c55706c7139436934365a46432b584f513271592f35504b2b5569306f6d4b4c50757239334745465659547a58776430336c7745467545527259654e7a434b38464f6946456d775956425675555471676f686256514d696b5034674769555766444d6859746951646974632f7555735465446a676a616741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f37275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141417146424d56455541414141795a737a2f4d77372f2f2f2f304e42466246776f70544a497a614e424f546b374c793873725973744f6564497156616f735a644258614a442f4141442f4c77436d57452f4e4b51757071616e2f61463956664e4c4d31753854574d6b2b506a372f7a4d725330744b4868346663334e79337437664377734a3565586d546b355072362b76464b51314b45676767505855794d6a4b656e7035746257332f3239556348427a2b764c622f66572f2f6548442b7070372b366555724b7973524552466a5932502f5755784663744259574668634477426c5555395759486b6636744c75414141427a456c455156526f67653359363036444d42694134627036574f64685536596955416271594e4e4e6e4b4c652f35324a5543627041637570662f7a65784d515136324f686443424345415242454152424550516675314a302f3944686c7a36654b554d6a525964484863446a4532564467516571414151515141414e67444e6a344351506a615a7955674358646c365366657453516f676678633341367a794556334f704b49414f7a725057464f2f7a466d746463484a614445485a31354e736b674a496967472b673674353478596758756e4d6b474235555173776d324e37454339616750685a4f4b6e36494259587a392b6765464a72514e393141387572584d6357494e37775531534374447751684d6f70616f4262585a442b486c6d57496d6b437672427a796938624657685644746c4d444e4d47344a62396c573141564f34422f454b74417a64647741524c447634427a727541355172674c2b4a77494c7335516d4f677a30596241796d41413131446334754762545838396a333466656962416c3032324455466c7338334f304f677863593672326241674133464166656a76594c59693232453176624f33582f67383364687a324232317a6d4f56336d6b6b5479323951787943536430594a434b337042674b486b4d48684b6b306c654c57764474726b67586a464c693565737a3942782b67366b4633322f79304778614e4e4945732f737753654d343371574a696c4f384839376d36623979537a66764a6d4152674141434347414e474a47663150746e3732435461762b62714f716a43336975444632712b687933372b74435759647051424145515241455152436b33546552454559374c6f71374d6741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f39275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414267414141415941674d414141436447645672414141414446424d5645564d615846436866524368665243686654307443505a4141414141335253546c4d4167464a456b47784e414141414c306c455156523441575041447867647742543342544446394155697568644336574e4b2f762f2f2f792b55676772436c53413037455756676c6d4546774141356559534578654377696741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3131272c2764696d675f3231275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414649414141425343414d4141414477386e4f70414141415946424d5645582f2f2f3933476b6c3247556a6d3074794b4e324a39496c43494e46393548557a31376648372b5071424b46584f714c7a392b2f796556337672322b547a36652f446c61336276383678655a6152516d75565358436a59594c77342b7135686143454c6c716f615972486e62505875636d2b6a71657262343751726344697939636a4536764241414144756b6c455156525968653259323961794b6853474253564555544655464866336635632f696943536c562b4e64624a4737306b6a6841636d7a41306142442f39394e4e502f325052754e434b36616345767948764d7130752f7778597a6a744c2f34513371425746487848374b72562f73746b6777614c506b48534b3049344531667731556847686939544d6235416b5662766d496d4753666f636b556f31316b62426c38566449496c7331314558654a416d2b51524c5772754d63354b526439464d6b59516e306b5a76546634714d4d7742385a504157535568634e49304b56584953713339486b727958517356715656565a5051705a357551724a4733536a69634951794f4d576a354f78546262784a53475349384475474e61386755796c686b43436752324c662b5362467058576c52346b5832474e3758506b666d59484841574331717865484a546e5478556a354f6e794c7a475a3043396277767a7a38696965307063686b6e365a79526c79424b6833696667626750506734622f44526c794f7835463958325154497856757739454d696a71534f6d4754464d626166467a4a4758324a4b4d68584632636b714c5039715633684f616855726d5a44394551626a7048787257317348644370686e4e544a446e76717450723130394e33596a6565686c323245372b38673330564f3257377a775937326b6432774f6f6677724d754e61776774706153782f6853526e534a4a764b72795a552f5157535949317543376d53784b4f3731665a424568634b68516b7a7675686a717758506b655767537151394356534f5754597933764745777a322b486d4f6c4d486d5975644930737970364b70494a553177544574506b56536f7177456536436d53354f6c59335235676235426b564568594e536449476772755a2b424c794870424a76306a4d7062384c4c764261386a566367385a6938514871765769396e595243556269752f7141344a4547634d493756724b337271365273493739674c7a74524c6a517370475644626b53506152626b566c78524d616473385132593331596248483748726b3630534e79746f7545714376644634494c4d533431306a4f63375847337051416a2b52355a726b6a76654d686f7265364f784743346b6a62555879794f546d544c78455032337964376c64793267533679794179796e59394957796865707542742b3836525a71425a774f367572354771674236527074755755586269644873616b4b72726a6f544a736d47483437467543626d7a7a4a6a5a63465449336b50434b6a54544131415848704b4b2f5972484a33336d744f68724e2b794e6e5935466e4d3135735742687443376b344a64393469776e45326d355a48576e446577466e745237473235357661547a684433576e6959376844684b45504c7a4d4759615365394f493453334a554f63565568716738523042623677324459756462747531352f6745626d376b53396b64376b6a767176613063455a4d706a504c342b524d49652b567066566375636d2b684a4a792b7168546b436339633439793768584d54713332794e53767849413078694f4c58424e676a67532b584c412b72304249476263734243746d663641624d52646139674d436b672f526e627263464b4a6d5377426444656162474352307654554a3237626a5a796d63424c7256355436727371454a6c44623778437171756459582f7a596f74346634394f58783865656139645050776e39394e4e502f34332b41627851504c6a33385835594141414141456c46546b5375516d4343273b76617220693d5b2764696d675f3133275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d41414144785067523541414141636c424d564558754d794c2f2f2f2f74494144744a513334747250754d522f74496762306a346a326f3537744b5250744667442b2b666a326e706a796533502b39765838337433754c5272764f536a765244627550433334757258765545583936756e316c354c734141443731644c3372367636794d587861562f30683444367a63727858564c796347667857457a76537a2f35774c3378596c6a796447355a6e6968724141414745456c455156526f67653261375a616a4b424347465270554e49716d51324b4d332b6e37763855315569674b5a6e706d31396b357537352f2b675331486f47694b4d70326e454f4844683036644f6a51763667414c555578333564583970384c3362324f426a7343306164724b446b6873682f7777775336626848735272514433527a395a714162375558634173624f546f4f71674c6e3030534a577848496e563158413634324e776a3041377a754e71514a475750346d5349436a4d6b4a6677765051347247424f774531684231754e744c76414231306c773039612f315271627156583866664433727944586e3461546236747069314366545a616677626e6c555873657837644c50456970795646732b7a64644563306b51324e45674334786d5977477a2f493844726252783168487a3557374267583241566a614e2b682f36355462414e74437a646e7765754644486e4454434d6c6454394869766e786841612b2b3843512f48466e4733676241592f775061647a52594a675957564d46757773674772376e58724779435234686d597a6c2b4c46526f4a79313134304c715432346530542f6d506530696f4d70317070686c346e6476674e65766448496f7a2f7947517759703148357070656f4c474b31756a6c6b42526a6371564779546f5230436b2f4e4c546f69362f774b7757477a776a654163656d436e5a6532445167756c65343545415a6c56737067784770474641544735663734416b5662366f6d326146624179377a645450694b57457931454e7337644156494844364b5a5a424d507a5a5863594b3942526f394b73675651445472366f6d38594e4e50706245326a74495947352b57706764496936494e2f4577356f7636716b507a38446a716a65384751692b4e3832683237547937776c65686176667765434c594c725148515a47774256763036485a53386445483538564c2b7a4f7371733534764a4e594a4f6f4f6546674f694663785a64686c4e574b767543356352736f45716b7048417547495a44305a34596f493272664967524233684d323355574a6f437338654d326d7876514e304642457356725a636546485062695336794f734273454e703775465535754e626d745a484674414d517957476a6c646354614e37614b5a322f62446e7743477a79457444657251754843697a68354155592b75695a3969325236576731762b54614435644a686348556937672f4f6e33736d71485265394252682b47386766336c4c6c5634766f354e4345645634685242774c6b5565315843444749362b6e5347733258732b3268634878537348797451686d2b4a786c35344252767658496f4f452b55373938487470617849634f2f5235685a74567535536a734a315a39375558633269333271696e386a34476e2f3177502b334b3574625237526573705457544c6e555775437349483264446a685639614f57617150346b6a31443261356e6c6d36343074594b52746d73636c6d4f494430594d47665664743351515364726d4c4d55754e63382f5279774d42505658696c586d4534764d686b53544c5a2f582b56376f64716261415161706e4d364b634377536f3062504871734d6a634a6e667858317472536873413347397974632b564a366a4b6b655437596161774e634447314f385067484c7542303854515079334b4a4b6362504352324144756b6e36746f7152464570567a6164366944746b61387255654a536654684375454f6f6563535932344d617832374c7748787a4279546c76686b51764b36586c49636d666177623353307253467037395241434d723145552b644d78704c6564453231412f46536a474c7a794e5a724b5939516471544e384f446a6e717a44453543485a37626745436f516f5259796f3038377a65366e2b4138784f70537334326f717067314f684241373150674d67684176326b4a597153786e62426954796b466a53715a6f6c5061566863676146616963384857394e71414c434b366f536743564b32594279524d504f5355466348756d6a6d79786446456864534e4534326d4732416849755454586d6d453666456536544d706943574a4d4533615137684e6f464f63623165516c30626e4c614c51467a64635a2f69526c4c445a52547358486c7441624367662f446e45524c704448584e696a68573841795851484278766541314e38774b394157304673447764463973795a734151627962426d577035556171506a6e7a66704b6c36336e554d36365a2b594e4675425541574c4255687a4f766d4934524b36757249415949734a6c323073314949477053725449784d645868524c716f6b4936426b7979424b7276536647626861383773504b612b3030744b33595a69336845726e4d337270556c7a737258755a726f6b5959674469556c7978526167535144582b6a54327a4234394f5a4559546a577464566d474a654d3451437a573132356e327747336c366258486546636c5a6f32793673477a425664632b77754a616c583732736a5846782b6b5467697274586576667831394235745433705879324743375a553037376a30386f314e4e5a4a6739713845472f7368354731696d6b48456d34533551524e33306230563845326f4765766d6d376b4e4d52592f5155555866426c5659454c5332784a4d61724f2b726c794145497063753151424c57465a7252345476394c77476d70495957665973334e704a4a3769376279524e4c566f7a4c446f54684b7934387153664c4362357a4638356932555a4550562f7072693255326c7a346d3156324b4e33474f537472742b587a7753686b6f4d6a4e706a7448347458482b527847753653676948547030364e436851332b492f674a2f6948506855784b5a387741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f3135275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141413156424d5645582f2f2f2f6a6641446a6567442b2f2f33696467442b2f6672686367447267514438367450366a774436376433392f5066376c41447468674431696744326a67443335395037394f763738655437307148706a525866625144716b437a3133735432343833686677446d6b555838324c50366f4450316c532f766d5437726b68623030375478794a767477492f737549626967436636776f58346e682f76304b726c6a546a6f6744583236642f77785a37686642726e706d3369677833747670546c6e316e6c70574c7172336a767a4c54706c32546e6a457a7670337a757771586f754a6278313862696e573770716f44706c46726b63786e707359336b6479626d6d555030723176347a4a4c367533583272453331707a6e796641416c4e7748774141414e4f6b6c455156526f67653161435865627568494749555143735931494d4574365578616e45474e6a77456e644e4c6b70625872372f332f536d7846657744464f2b3972653838343731546c3144466f2b7a66624e534b346b2f576c2f6d6954706a764c76417159732f6e634259325a6e36722b45705a704f6b6b526c3551572f46555a426d366e635361496f4468304c704d75383548666953524a33677a444b484175526c314f4657776c3166694f674730616861786e71536a47347362793965372f696269616276774e4b6b525365784a4659652f7268357537526c5054567a6430484b6244693334496f57566b5563496c7a2b4d72763339383833457553385948666378377965577239636a676c695630442f6b342f6d737370574f2b653636725264446d426c4e37785834786e785948424a5555314832372b2f6d67595169442b734a713647495768593651706c33346c366269527a706533793655424b452f4c6577752b54766e7138655a6d4b5a416a79357a4d6a562b495a325a63637039752f6e34414b62677133542f64664870554662363865586f537174526a3157585672304a554a434d7955484f3371395855584b71414b443038725a61415051554f554666674e30346b42617871614f46486d3272736b614d6942655953564b644d5665582b34656239453677364258633137724558465875376c494b466c4c487352385341565254446955732f54564f2f7142795541324d503133534d3230635142722f6533393138584973396658783457454a77726a37646656436c49414d6d2f333545574a6f48706363596f35544370383238644747756a6349742f75456a35782f76707841504b78302b5631504f7265586a337a667751706f7570616b687157456d56665148614e55745a45706b797168334d523566654d7932575435763071756c532b7239394f6e75426f4d4e325852353933674c2b6c773933433978543462705a4658783643564759627666475278475261684d364b514954554f465a7568426d524a477834474b4b6f5552717252386646677041516362547a393875734e34554131754a6d57614531414c4a64544f72474c6d66422b694a594e3065597955714268476f306f6556446c6a4261416c316e513152564c6a397838665079355269397930544366326852474954414253546f73713043635439377438315a68545670677730417a6e5256484d4936665256557a735757796f7a764c6d41373551514c59555a654d414e674555496f4e654743506a4b6a41744c756d754b6166485357367a47576353414942546778454a7754586b306855305855337373614d75507a6f636542706b57306d75365251455147515a4a5a756c5253783242794670756645386d666d39524f3434727237656a6d4c6f7742636c6f374973316b46513667665971382f424f4a6c6a38506550364a4f4b75386852697a434b73627859424d337975684e557151632b586c5330364b4e564d70756b666855366131413342654667475a764b6e6b647444424130494d696432677755646676707653475a3551785641426133535a3370584b774d6476426e464e2b4467744f616c54324151686a302f334673416c63795849616d6c646941345559467247485861466770385364317348726931687832497850687a346e516f384b64796f4d6c7945597a6146453250317a4a4351386a73676544714a2f46384a336d6d623774567430596c4a64487770526d4465487470417a5870537864514671436e5668426e446236625a7948356e355a5a566e655536334738364b2b386968722f47514738685549703570516b6f574e2f536f3070616952696b5179557970304d733461492b6978543344487370444c5330736f347a6a475645425a654241526d77484c627979775142724a636c7330576d656754726541674335637a6c4f663179674c545350554a444968452f71464c55304b384952326367706d374768394448566d56736f32556d39513037576e676a337a79704855424879504641587a4f5a71496f5249555351304b4559516757516f6875423936536b624a41566f3158644f4569712f784b46555059567255474754394151717264456b76684d6f4c6e734c6e584e6a4f4c46435473435566597643414b416f75394c4a6142616468784b766e433064764a716d7863417268414c5278426b595758457053384c7859435169424f67495330344b674c6d6b6148366d415935496e69643539742b594b614f6b3852426f4e476d334b746c66456933672b526a6f68794b62365975794471424e427a55614a5157446e432f306730726f5a637a7333793636755a6449347449416c525741347775666f4c425231764b71363155526f452b5253515a57576a7a7970566a614d6b6550584b444f344b6f776f366567625a524f71417852505a737a50664d51726d7a4a61444c55574535446172674e3834503745456d6f67496c526579516e6f536c62534b617a4b4f49497a5546586e744b454b4975646774424c4e704b7933696145497a6b396e4651686b2b5a6a52462f435938316678444e77536a77377241644a6f4261456f484b3851626d68594a684237737a3048765a546c4652786169472b7046534835556573317a635777304d6442377744754c487749725654486f45344b344473354c7a4a584646475a54443343434a59676754716e4e483431772f4b7342713752493538654f65594144356432444869574c33684863456770504e2b7462524763784450564f614842712f717357424942305537314e44326f6a653138437a626b354f766f4545514e365263316d3077457363774e55436b4162707054654634644748754c38594b526770656f564f36422f6a6331683246425357426961644275657472435130677952796e4e4259526c376b4a55554c6f7450335655425756374b755a7a34413861524d4b546e5a6d2f586439504a396977376c336e625253315a4774713230484f49714d3532467649526f51754e6b3463436b3244545a554f6e7544305764794d4356697838656c4e496f4d3932757969456c63456b726e6d377162794578555a556b2b77325359576c4e746a537967327838436d324153615752533132475735325542696232714f6c684249595153344558705355596e6c2f72794b36367277476b716c644f343258474241766f72567459534f344e3563654b496f594c4d696c367661677856537652454c2f73563270625941642b6f4476302f6e5a73556735775563366d48466764494a3868415269574d3246304b61516f556237634555396851466872736f2f43696569486f6e72584d303455374851496178514e7a774b476c68676d4e414a7471716a417450424f71454d5978556a6d6e57744c3159646c755559516d6c43474e6a483072773962364a434b2b7432786f5162736865776e5a55744b6141587550626a72546d584463555a346e41593432553873534459544e33742f756b71442f58496c74646c6339466d712f334c644c594267386a57325433744b35434b4a69686d55465531716d6f4d325235374a6c626c30326174586b6c6c4e5559414136656d36556b4d362f6e4f5661487773316d6564346b764265485954316c45435237784771345765566a535261466a754d45436251677159786d70795a6b4b747155675836727a6e564c767832316f7444304638464c796a596639555a6174524f7a687557416a39554146416175726b50524e747659533958424138453865627972354257706d70464f30424c6d69663439366b4f37624f6d47367934495a4f716362354b6c6f613831596b4174587252337937757370455232687947673673694f33797a6f77514943527751353549597143397a3167716f494f5967513072706a656e454d4d7964647644537a6a755a4a7330724a2b6b5167693978414356535969626d656f70666f664939746d62715153725848756f5630504339543175564d4d516e4f476e4a64786c41494d4b5152747569627265427070447435612f4365746b2f534f314f497542512b554279706c51713276397669754157372f747763457072513337773869756441475541365732356e79694f41614d645a366b4d5431774e6b632f4b43343246777a43497847344f564f3468302f446f6768634e313572696d446f514441524c45425a366e6f486e56676668744e33644771705475696167663878717347576764384937694651684d427a6a47354b2f39504d4342712b7439382f6366384646435173756675423958387a3054436f55647538576b6e754346677a72346a7173575657516a7236745463757a32792f2b357133456a6878716b5342685a6b306254614e3476346b39656a484e303745714a684950765774702f32763676726c4e62737a4754637367776534332f334c72484d562b2b2b71565837582f612f3262375152736648373435632f534d4f6a382f747a70394f727a5a6352322f684d645737776b386e6a514c4e6c39664c4b75494b514a5076317750627072686e702b6230764230654e6d5a64546b38506233657a7462666e5a362b32394b47496e71666d306a344446387644346877636746543849767a35757a30644c6462342f6b4d73553750686c3836347939507a3971414d4f6c646936663432646e5a6c57416e3638335a326274447565766b4c2b6941762f71625578693841377947523967687648767542355432416457764d45316f2f424b6d667a356754365542564b527a4242687541666b376741494a34632f587a6a365041307268734f6c57727358307778494b6c594a4570322b2b624731344d6b4c5a54416c322f4f616b50523441523063415964377050336945523145504d6e38624d4f784d504c7343306237412b70304566446b364b71454651434d41636c476a42354e514737436c6767336735664273314e454d7642683241643932317232472f6e4f7830654868484e5143484c595338516251484936473139327747413362586a6f616451477848397a736e2b466f644c67304f666c724e477741523375414977513865547361646c537a447a6a73416972577851676d5771505273443663764c65417346426270594f52414c7a385042792b365155456c513648585543706867484f35546363645a4339476b4246414c596c58414e2b67666544397333554869444d3367504541654831743948686f4e67434b732b48415a2f4434556872543330464550783041455a4d766f314750655551417236786f756774614c316c5a584d4e57434c416c355a7958675530726f624469355042384f74687641625130595a6f3552616c624143726b7776776d7462345142747062634442384f31652b523343694f543532376c307544574173487a582f5465416f54556544713561485945323048592f34756a6a7757425051736b5a444c546e685051567443642f44515a6a52394d47304671324d7545524152336c737a6277576c346a414c63624f41536f587730474638355833704e685432444b57502f7935545073712b3443347630386c36344249646831584c3471496337346c6a67393578774641532f575579396167466f4443446d66447251575356322b4b7145596374316236514b67686b426d48364144674e4865616c304a3932585259613766653237635347694f4e61304e364347674969574a4b512b3063696645433556714c77417438496a443139685349356c32434642754a4e5249634b567072534e6b6f476d76417372616f502f5132512b6f43554161786c547a646e6544416446494731445458746751414c556a67424e74432b6931415455425346695a554533626364424c77497344456d72392f77477141376a7a4c484d394b556b632f434670393139453967466c4d745a6a614e4675376d7541736a5a6541354979324f775741655653614e4c305a46707570516a7765726b4c364e68346f3746547245586b3434414541546e2b6173593278333246343732744c616a4838676c4a6577454a47627455396d537641796a334137707251476e426d6d7668426c434b6d4f77316c305a4741524a733577633273376658765a492b5933622b516b4a343767384c39784e4d77533971525a6d3975394177596e6755706c4f634d41793363617a44512b74734155384a666f54687a6f5947767579394e75415a39445a6654566971355848773246774a39303374613072727336667a5545632f7a7038542b702f32702f312f742f38412b724a44466c344139755541414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3137275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416231424d5645575a4141442f2f2f2b574141435a45684c7a354f535341414474323976713164585772713743686f61765656586e304e432f666e372b2b2f763538764b705330754e414143694f546e66774d446275626d5a4367717153456a323675713965586e52704b53474141446b794d6969516b4b36635847684e44544a6c4a537457566d644b69716149534764485232775947433161576b305454574c4141414376306c455156526f6765325759584f6a4942434764654e714c4e56456a4d6247746b6d622f502f665747424230574454752f4e6d626d37322f59514c374f4d43757842464c42614c78574b7857437757693856697356697366307962425230696d4a73414944544a39335a59374848434e416d7150554c547a6d785a2f626f787944642f5576734f6f7a63784759386859426d486c654d755945324f326b6c5654477744454d36547765304b77446a756c5074714f756e7369466a2f45664170334b465763427068584476484d4c5758693843796c764a6b42376c6d6a7057553076374f53526c623636626f34564c4c4f7445687044546e772f4a793353316f5746624c4c53774278517675477a744f4e63316135684168496e6b734b745838644f76566f65725a622f562f584d6c794a4d2b5971625a384a64767a486b4d42456a446252324158554a30736175626143566a677a767549552f32424769695177685947434f5a487a3065374a6148776667324975563331616744754f7a4a56777a2b30384c7761454b357545776367374f6959644b5a667539726962774450433043374f3855594964684e31716b495a6933376878466d716954495759537471545233533270505462735a675743585761556943724f2f6a34415267436d524870427331476d4250514b364c5978504f414b6a4379574f536b57546e506c6a6f4f5036774e46716765393533746b7a47736533385a53717543516470452f5535366634694659425469534e4a77654547316d504a6b4d6b7267394d71617734594252524b6c494e754d4c6177474a3767436b514b52564e5257763159566746574769565a646f3177385867674e434d4662746243316a6365715871416a6834475a63557831586f56774d3264374e486f4574464e64643872674c6366514e307165694b30393848326c5455536668546f436f6a376a355556364f37783754563359657a797733785252394f6f63786a4b756f455656507339625230487872506653704552754e4b49635357526a6171535365777949536f76542b2b6e49526f615843717468636f46585553536945536370514963566f737037506e556f6f42612b4c4e39743830543242547354334d58306a46556f687a59425943706a3751383675416b626b5664524a4f6763464831426f52556970576438446c434a736b3955585076706c56657341334d5a695478747939535772324749552f4a666a794a754a4d49665053684f453734476d4a78324b78574377576938566973566773466f7646597630332b67492b6453615a504562466d6741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f3139275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220653d27704d524f586250624472505339414f426d4b50594251273b2866756e6374696f6e28297b76617220613d652c623d77696e646f772e706572666f726d616e6365262677696e646f772e706572666f726d616e63652e6e617669676174696f6e3b622626323d3d622e74797065262677696e646f772e70696e6728222f67656e5f3230343f63743d6261636b627574746f6e2665693d222b61293b7d292e63616c6c2874686973293b7d2928293b2866756e6374696f6e28297b2866756e6374696f6e28297b676f6f676c652e637363743d7b7d3b676f6f676c652e637363742e70733d27414f765661773144704144365477704f5055674e344e6f72467065555c7832367573745c78336431353635353239363336323834373838273b7d2928293b7d2928293b2866756e6374696f6e28297b2866756e6374696f6e28297b676f6f676c652e637363742e72643d747275653b7d2928293b7d2928293b2866756e6374696f6e28297b77696e646f772e78703d66756e6374696f6e2861297b66756e6374696f6e206528682c662c67297b72657475726e227870222b282278223d3d663f2263223a227822292b677d666f722876617220623d2f5c62787028787c6329285c643f295c622f3b613b297b76617220633d612e636c6173734e616d652c643d632e6d617463682862293b69662864297b612e636c6173734e616d653d632e7265706c61636528622c65293b6966282263223d3d645b315d29666f7228613d612e676574456c656d656e747342795461674e616d652822696d6722292c623d303b623c612e6c656e6774683b2b2b6229696628633d615b625d2c643d632e6765744174747269627574652822646174612d6c6c222929632e7372633d642c632e72656d6f76654174747269627574652822646174612d6c6c22293b627265616b7d613d612e706172656e74456c656d656e747d7d3b7d2928293b676f6f676c652e647274792626676f6f676c652e6472747928293b3c2f7363726970743e3c2f626f64793e3c2f68746d6c3e", + "rawHeaders": [ + "Content-Type", + "text/html; charset=ISO-8859-1", + "Date", + "Sat, 10 Aug 2019 01:21:31 GMT", + "Expires", + "-1", + "Cache-Control", + "private, max-age=0", + "P3P", + "CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\"", + "Server", + "gws", + "X-XSS-Protection", + "0", + "X-Frame-Options", + "SAMEORIGIN", + "Set-Cookie", + "1P_JAR=2019-08-10-01; expires=Mon, 09-Sep-2019 01:21:31 GMT; path=/; domain=.google.com", + "Set-Cookie", + "CGIC=IiFhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L3BsYWluLCAqLyo; expires=Thu, 06-Feb-2020 01:21:31 GMT; path=/complete/search; domain=.google.com; HttpOnly", + "Set-Cookie", + "CGIC=IiFhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L3BsYWluLCAqLyo; expires=Thu, 06-Feb-2020 01:21:31 GMT; path=/search; domain=.google.com; HttpOnly", + "Set-Cookie", + "NID=188=vTMutucOBO-Yl5bpVtVnzkN1voOukQ24RkD0wuuzeNL_BDPMEB90MqBF06HFaILh_fs-PO8JGLhIjkSb3nxl9Rzf8L7CxJtk_yJF0aEgi2znY0rMT_dQr6_5tYfVNKU9u0d2BoXOVOWHEN3ZzaD7q6yRUb44yH3vjL0kue6Ki0s; expires=Sun, 09-Feb-2020 01:21:31 GMT; path=/; domain=.google.com; HttpOnly", + "Accept-Ranges", + "none", + "Vary", + "Accept-Encoding", + "Connection", + "close" + ], + "responseIsBinary": true + } +] \ No newline at end of file diff --git a/packages/opentelemetry-plugin-http/test/http-disable.test.ts b/packages/opentelemetry-plugin-http/test/http-disable.test.ts index fcd7d8912d..74b7392d1a 100644 --- a/packages/opentelemetry-plugin-http/test/http-disable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-disable.test.ts @@ -74,6 +74,9 @@ describe('HttpPlugin', () => { httpTextFormat, }); before(() => { + nock.cleanAll(); + nock.enableNetConnect(); + plugin.enable(http, tracer); server = http.createServer((request, response) => { response.end('Test Server Response'); @@ -86,14 +89,12 @@ describe('HttpPlugin', () => { }); beforeEach(() => { - nock.cleanAll(); tracer.startSpan = sinon.spy(); tracer.withSpan = sinon.spy(); tracer.recordSpanData = sinon.spy(); }); afterEach(() => { - nock.cleanAll(); sinon.restore(); }); diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/http-enable.test.ts index 2c25116e80..a5613b874d 100644 --- a/packages/opentelemetry-plugin-http/test/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-enable.test.ts @@ -50,11 +50,7 @@ function doNock( .reply(httpCode, respBody); } -export const customAttributeFunction = ( - span: Span, - request: http.ClientRequest | http.IncomingMessage, - response: http.IncomingMessage | http.ServerResponse -): void => { +export const customAttributeFunction = (span: Span): void => { span.setAttribute('span kind', SpanKind.CLIENT); }; @@ -107,9 +103,10 @@ describe('HttpPlugin', () => { after(() => { server.close(); + plugin.disable(); }); - it('should generate valid span (client side and server side)', done => { + it('should generate valid spans (client side and server side)', done => { httpRequest .get(`http://${hostname}:${serverPort}${pathname}`) .then(result => { @@ -138,12 +135,14 @@ describe('HttpPlugin', () => { for (let i = 0; i < httpErrorCodes.length; i++) { it(`should test span for GET requests with http error ${httpErrorCodes[i]}`, async () => { const testPath = '/outgoing/rootSpan/1'; + doNock( hostname, testPath, httpErrorCodes[i], httpErrorCodes[i].toString() ); + const isReset = audit.processSpans().length === 0; assert.ok(isReset); await httpRequest @@ -280,7 +279,7 @@ describe('HttpPlugin', () => { }); } - it('should create a span for GET requests and add propagation headers', async () => { + it('should create a rootSpan for GET requests and add propagation headers', async () => { nock.enableNetConnect(); const spans = audit.processSpans(); assert.strictEqual(spans.length, 0); diff --git a/packages/opentelemetry-plugin-http/test/http-package.test.ts b/packages/opentelemetry-plugin-http/test/http-package.test.ts new file mode 100644 index 0000000000..5bfa261a88 --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/http-package.test.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2019, 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 { NoopLogger } from '@opentelemetry/core'; +import { NodeTracer } from '@opentelemetry/node-tracer'; +import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; +import { SpanKind, Span } from '@opentelemetry/types'; +import * as assert from 'assert'; +import * as http from 'http'; +import * as nock from 'nock'; +import { plugin } from '../src/http'; +import { assertSpan } from './utils/assertSpan'; +import { DummyPropagation } from './utils/DummyPropagation'; +import { ProxyTracer } from './utils/ProxyTracer'; +import { SpanAuditProcessor } from './utils/SpanAuditProcessor'; +import * as url from 'url'; +import axios, { AxiosResponse } from 'axios'; +import * as superagent from 'superagent'; +import * as got from 'got'; +import * as request from 'request-promise-native'; + +const audit = new SpanAuditProcessor(); + +export const customAttributeFunction = (span: Span): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +describe('Packages', () => { + describe('get', () => { + const scopeManager = new AsyncHooksScopeManager(); + const httpTextFormat = new DummyPropagation(); + const logger = new NoopLogger(); + const realTracer = new NodeTracer({ + scopeManager, + logger, + httpTextFormat, + }); + const tracer = new ProxyTracer(realTracer, audit); + beforeEach(() => { + audit.reset(); + }); + + before(() => { + plugin.enable(http, tracer); + }); + + after(() => { + // back to normal + nock.cleanAll(); + nock.enableNetConnect(); + }); + + let resHeaders: http.IncomingHttpHeaders; + [ + { name: 'axios', httpPackage: axios }, //keep first + { name: 'superagent', httpPackage: superagent }, + { name: 'got', httpPackage: { get: async (url: string) => got(url) } }, + { + name: 'request', + httpPackage: { get: async (url: string) => request(url) }, + }, + ].forEach(({ name, httpPackage }) => { + it(`should create a span for GET requests and add propagation headers by using ${name} package`, async () => { + if (process.versions.node.startsWith('12') && name === 'got') { + // got complains with nock and node version 12+ + // > RequestError: The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type function + // so let's make a real call + nock.cleanAll(); + nock.enableNetConnect(); + } else { + nock.load(__dirname + '/fixtures/google.json'); + } + + const urlparsed = url.parse( + `http://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8` + ); + const result = await httpPackage.get(urlparsed.href!); + if (!resHeaders) { + const res = result as AxiosResponse<{}>; + resHeaders = res.headers; + } + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf(`GET ${urlparsed.pathname}`) >= 0); + + const span = spans[0]; + const validations = { + hostname: urlparsed.hostname!, + httpStatusCode: 200, + httpMethod: 'GET', + pathname: urlparsed.pathname!, + path: urlparsed.path, + resHeaders, + }; + assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); + assertSpan(span, SpanKind.CLIENT, validations); + }); + }); + }); +}); diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts index b91dbcbad3..f4f32cadc4 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -137,36 +137,33 @@ describe('Utils', () => { }); describe('isIgnored()', () => { - let satisfiesPatternSpy: sinon.SinonSpy; beforeEach(() => { - satisfiesPatternSpy = sinon.spy(Utils, 'satisfiesPattern'); + Utils.satisfiesPattern = sinon.spy(); }); afterEach(() => { sinon.restore(); }); - it('should call satisfiesPattern', () => { - Utils.isIgnored('/test/1', {}, ['/test/11']); + it('should call isSatisfyPattern, n match', () => { + const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); + assert.strictEqual(answer1, false); assert.strictEqual( (Utils.satisfiesPattern as sinon.SinonSpy).callCount, 1 ); }); - it('should call satisfiesPattern, match', () => { - satisfiesPatternSpy.restore(); - const answer1 = Utils.isIgnored('/test/1', {}, ['/test/1']); - assert.strictEqual(answer1, true); - }); - - it('should call satisfiesPattern, no match', () => { - satisfiesPatternSpy.restore(); + it('should call isSatisfyPattern, match', () => { const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); assert.strictEqual(answer1, false); + assert.strictEqual( + (Utils.satisfiesPattern as sinon.SinonSpy).callCount, + 1 + ); }); - it('should not call satisfiesPattern', () => { + it('should not call isSatisfyPattern', () => { Utils.isIgnored('/test/1', {}, []); assert.strictEqual( (Utils.satisfiesPattern as sinon.SinonSpy).callCount, diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index 217ae67308..cffc62f494 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2019, 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 { SpanContext, HttpTextFormat } from '@opentelemetry/types'; import * as http from 'http'; diff --git a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts index 467c4c3685..af34f5b50c 100644 --- a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts +++ b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2019, 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 { SpanAuditProcessor } from './SpanAuditProcessor'; import { Span, diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts index c8173a6fca..bdc2dca1fb 100644 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2019, 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, Status, @@ -23,6 +7,7 @@ import { Event, } from '@opentelemetry/types'; +// } export interface SpanAudit { spanContext: SpanContext; attributes: Attributes; @@ -30,7 +15,7 @@ export interface SpanAudit { ended: boolean; events: Event[]; links: Link[]; - parentId?: string; + parentSpanId?: string; kind: SpanKind; status: Status; startTime: number; diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts index a07ab892d2..53749a2d7b 100644 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2019, 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 { Span } from '@opentelemetry/types'; import { SpanAudit } from './SpanAudit'; diff --git a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts index a1825f666a..3eb60f2ce0 100644 --- a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2019, 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 } from '@opentelemetry/types'; import * as assert from 'assert'; import * as http from 'http'; @@ -30,9 +14,9 @@ export const assertSpan = ( httpStatusCode: number; httpMethod: string; resHeaders: http.IncomingHttpHeaders; - reqHeaders: http.OutgoingHttpHeaders; hostname: string; pathname: string; + reqHeaders?: http.OutgoingHttpHeaders; path?: string; } ) => { @@ -74,15 +58,21 @@ export const assertSpan = ( span.status, Utils.parseResponseStatus(validations.httpStatusCode) ); + assert.ok(span.startTime < span.endTime); assert.ok(span.endTime > 0); - const userAgent = validations.reqHeaders['user-agent']; - if (userAgent) { - assert.strictEqual(span.attributes[Attributes.HTTP_USER_AGENT], userAgent); + if (validations.reqHeaders) { + const userAgent = validations.reqHeaders['user-agent']; + if (userAgent) { + assert.strictEqual( + span.attributes[Attributes.HTTP_USER_AGENT], + userAgent + ); + } } if (span.kind === SpanKind.SERVER) { - assert.strictEqual(span.parentId, DummyPropagation.SPAN_CONTEXT_KEY); + assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY); } }; diff --git a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts index 88d41c2e6d..46cd74fe95 100644 --- a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts +++ b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2019, 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 * as http from 'http'; import * as url from 'url'; import { RequestOptions } from 'https'; From 147f8a94c7627a5e4cbd9a1ac7a82a9ffa7c66de Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Sun, 11 Aug 2019 15:17:53 -0400 Subject: [PATCH 10/18] fix: add mayurkale22 recommendations Signed-off-by: Olivier Albertini --- .../opentelemetry-plugin-http/package.json | 7 +- .../{attributes.ts => attributeNames.ts} | 2 +- .../opentelemetry-plugin-http/src/http.ts | 104 +++++++++--------- .../src/models/spanFactory.ts | 17 --- .../opentelemetry-plugin-http/src/utils.ts | 8 +- .../test/fixtures/google.json | 2 +- .../test/utils.test.ts | 41 ++++++- .../test/utils/DummyPropagation.ts | 15 +++ .../test/utils/ProxyTracer.ts | 18 +++ .../test/utils/SpanAudit.ts | 17 ++- .../test/utils/SpanAuditProcessor.ts | 16 +++ .../test/utils/assertSpan.ts | 32 ++++-- .../test/utils/httpRequest.ts | 16 +++ 13 files changed, 204 insertions(+), 91 deletions(-) rename packages/opentelemetry-plugin-http/src/enums/{attributes.ts => attributeNames.ts} (97%) delete mode 100644 packages/opentelemetry-plugin-http/src/models/spanFactory.ts diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index 0db289a6f7..e7d7829046 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -38,16 +38,15 @@ "access": "public" }, "devDependencies": { + "@types/got": "^9.6.6", "@types/mocha": "^5.2.7", "@types/nock": "^10.0.3", "@types/node": "^12.6.9", + "@types/request-promise-native": "^1.0.16", "@types/semver": "^6.0.1", "@types/shimmer": "^1.0.1", "@types/sinon": "^7.0.13", - "@types/axios": "0.14.0", - "@types/got": "9.6.6", - "@types/superagent": "4.1.3", - "@types/request-promise-native": "1.0.16", + "@types/superagent": "^4.1.3", "@opentelemetry/scope-async-hooks": "^0.0.1", "@opentelemetry/scope-base": "^0.0.1", "axios": "^0.19.0", diff --git a/packages/opentelemetry-plugin-http/src/enums/attributes.ts b/packages/opentelemetry-plugin-http/src/enums/attributeNames.ts similarity index 97% rename from packages/opentelemetry-plugin-http/src/enums/attributes.ts rename to packages/opentelemetry-plugin-http/src/enums/attributeNames.ts index 024477a6c1..8863dc9b0a 100644 --- a/packages/opentelemetry-plugin-http/src/enums/attributes.ts +++ b/packages/opentelemetry-plugin-http/src/enums/attributeNames.ts @@ -18,7 +18,7 @@ * Attributes Names according to Opencensus HTTP Specs since there is no specific OpenTelemetry Attributes * https://github.com/open-telemetry/opentelemetry-specification/blob/master/work_in_progress/opencensus/HTTP.md#attributes */ -export enum Attributes { +export enum AttributeNames { HTTP_HOSTNAME = 'http.hostname', // NOT ON OFFICIAL SPEC ERROR = 'error', diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index b671a905cb..b8c264bbf2 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -21,6 +21,7 @@ import { SpanOptions, Logger, Tracer, + Attributes, } from '@opentelemetry/types'; import { ClientRequest, @@ -41,9 +42,8 @@ import { HttpRequestArgs, } from './types'; import { Format } from './enums/format'; -import { Attributes } from './enums/attributes'; +import { AttributeNames } from './enums/attributeNames'; import { Utils } from './utils'; -import { SpanFactory } from './models/spanFactory'; /** * Http instrumentation plugin for Opentelemetry @@ -51,24 +51,18 @@ import { SpanFactory } from './models/spanFactory'; export class HttpPlugin extends BasePlugin { static readonly component = 'http'; options!: HttpPluginConfig; - - // TODO: BasePlugin should pass the logger or when we enable the plugin protected _logger!: Logger; protected readonly _tracer!: Tracer; - /** - * Used to create span with default attributes - */ - protected _spanFactory!: SpanFactory; constructor(public moduleName: string, public version: string) { super(); // TODO: remove this once a logger will be passed + // https://github.com/open-telemetry/opentelemetry-js/issues/193 this._logger = new NoopLogger(); } /** Patches HTTP incoming and outcoming request functions. */ protected patch() { - this._spanFactory = new SpanFactory(this._tracer, HttpPlugin.component); this._logger.debug( 'applying patch to %s@%s', this.moduleName, @@ -131,7 +125,7 @@ export class HttpPlugin extends BasePlugin { */ protected _getPatchIncomingRequestFunction() { return (original: (event: string, ...args: unknown[]) => boolean) => { - return this.incomingRequestFunction(original); + return this._incomingRequestFunction(original); }; } @@ -141,7 +135,7 @@ export class HttpPlugin extends BasePlugin { */ protected _getPatchOutgoingRequestFunction() { return (original: Func): Func => { - return this.outgoingRequestFunction(original); + return this._outgoingRequestFunction(original); }; } @@ -172,14 +166,15 @@ export class HttpPlugin extends BasePlugin { * span when the response is finished. * @param request The original request object. * @param options The arguments to the original function. + * @param span representing the current operation */ - private getMakeRequestTraceFunction( + private _getMakeRequestTraceFunction( request: ClientRequest, options: ParsedRequestOptions, span: Span ): Func { return (): ClientRequest => { - this._logger.debug('makeRequestTrace'); + this._logger.debug('makeRequestTrace by injecting context into header'); const propagation = this._tracer.getHttpTextFormat(); const opts = Utils.getIncomingOptions(options); @@ -202,26 +197,27 @@ export class HttpPlugin extends BasePlugin { : null; const host = opts.hostname || opts.host || 'localhost'; - span.setAttributes({ - [Attributes.HTTP_URL]: `${opts.protocol}//${opts.hostname}${opts.path}`, - [Attributes.HTTP_HOSTNAME]: host, - [Attributes.HTTP_METHOD]: method, - [Attributes.HTTP_PATH]: opts.path || '/', - }); + + const attributes: Attributes = { + [AttributeNames.HTTP_URL]: `${opts.protocol}//${opts.hostname}${opts.path}`, + [AttributeNames.HTTP_HOSTNAME]: host, + [AttributeNames.HTTP_METHOD]: method, + [AttributeNames.HTTP_PATH]: opts.path || '/', + }; if (userAgent) { - span.setAttribute(Attributes.HTTP_USER_AGENT, userAgent); + attributes[AttributeNames.HTTP_USER_AGENT] = userAgent; } if (response.statusCode) { - span - .setAttributes({ - [Attributes.HTTP_STATUS_CODE]: response.statusCode, - [Attributes.HTTP_STATUS_TEXT]: response.statusMessage, - }) - .setStatus(Utils.parseResponseStatus(response.statusCode)); + attributes[AttributeNames.HTTP_STATUS_CODE] = response.statusCode; + attributes[AttributeNames.HTTP_STATUS_TEXT] = + response.statusMessage; + span.setStatus(Utils.parseResponseStatus(response.statusCode)); } + span.setAttributes(attributes); + if (this.options.applyCustomAttributesOnSpan) { this.options.applyCustomAttributesOnSpan(span, request, response); } @@ -238,7 +234,7 @@ export class HttpPlugin extends BasePlugin { }; } - private incomingRequestFunction( + private _incomingRequestFunction( original: (event: string, ...args: unknown[]) => boolean ) { const plugin = this; @@ -278,10 +274,7 @@ export class HttpPlugin extends BasePlugin { spanOptions.parent = spanContext; } - const span = plugin._spanFactory.createInstance( - `${method} ${pathname}`, - spanOptions - ); + const span = plugin._startHttpSpan(`${method} ${pathname}`, spanOptions); return plugin._tracer.withSpan(span, () => { plugin._tracer.wrapEmitter(request); @@ -306,31 +299,28 @@ export class HttpPlugin extends BasePlugin { const userAgent = (headers['user-agent'] || headers['User-Agent']) as string; - span.setAttributes({ - [Attributes.HTTP_URL]: Utils.getUrlFromIncomingRequest( + const attributes: Attributes = { + [AttributeNames.HTTP_URL]: Utils.getUrlFromIncomingRequest( requestUrl, headers ), - [Attributes.HTTP_HOSTNAME]: hostname, - [Attributes.HTTP_METHOD]: method, - }); + [AttributeNames.HTTP_HOSTNAME]: hostname, + [AttributeNames.HTTP_METHOD]: method, + [AttributeNames.HTTP_STATUS_CODE]: response.statusCode, + [AttributeNames.HTTP_STATUS_TEXT]: response.statusMessage, + }; if (requestUrl) { - span.setAttributes({ - [Attributes.HTTP_PATH]: requestUrl.path || '/', - [Attributes.HTTP_ROUTE]: requestUrl.pathname || '/', - }); + attributes[AttributeNames.HTTP_PATH] = requestUrl.path || '/'; + attributes[AttributeNames.HTTP_ROUTE] = requestUrl.pathname || '/'; } if (userAgent) { - span.setAttribute(Attributes.HTTP_USER_AGENT, userAgent); + attributes[AttributeNames.HTTP_USER_AGENT] = userAgent; } span - .setAttributes({ - [Attributes.HTTP_STATUS_CODE]: response.statusCode, - [Attributes.HTTP_STATUS_TEXT]: response.statusMessage, - }) + .setAttributes(attributes) .setStatus(Utils.parseResponseStatus(response.statusCode)); if (plugin.options.applyCustomAttributesOnSpan) { @@ -345,7 +335,7 @@ export class HttpPlugin extends BasePlugin { }; } - private outgoingRequestFunction( + private _outgoingRequestFunction( original: Func ): Func { const plugin = this; @@ -390,21 +380,18 @@ export class HttpPlugin extends BasePlugin { // the first operation, therefore we create a span and call withSpan method. if (!currentSpan) { plugin._logger.debug('outgoingRequest starting a span without context'); - const span = plugin._spanFactory.createInstance( - operationName, - spanOptions - ); + const span = plugin._startHttpSpan(operationName, spanOptions); return plugin._tracer.withSpan( span, - plugin.getMakeRequestTraceFunction(request, optionsParsed, span) + plugin._getMakeRequestTraceFunction(request, optionsParsed, span) ); } else { plugin._logger.debug('outgoingRequest starting a child span'); - const span = plugin._spanFactory.createInstance(operationName, { + const span = plugin._startHttpSpan(operationName, { kind: spanOptions.kind, parent: currentSpan, }); - return plugin.getMakeRequestTraceFunction( + return plugin._getMakeRequestTraceFunction( request, optionsParsed, span @@ -412,6 +399,15 @@ export class HttpPlugin extends BasePlugin { } }; } + + private _startHttpSpan(name: string, options: SpanOptions) { + return this._tracer + .startSpan(name, options) + .setAttribute(AttributeNames.COMPONENT, HttpPlugin.component); + } } -export const plugin = new HttpPlugin('http', process.versions.node); +export const plugin = new HttpPlugin( + HttpPlugin.component, + process.versions.node +); diff --git a/packages/opentelemetry-plugin-http/src/models/spanFactory.ts b/packages/opentelemetry-plugin-http/src/models/spanFactory.ts deleted file mode 100644 index 87a41c9b0c..0000000000 --- a/packages/opentelemetry-plugin-http/src/models/spanFactory.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Span, SpanOptions, Tracer } from '@opentelemetry/types'; -import { Attributes } from '../enums/attributes'; -/** - * Create span with default attributes - */ -export class SpanFactory { - private readonly _tracer: Tracer; - constructor(tracer: Tracer, public component: string) { - this._tracer = tracer; - } - - createInstance(name: string, options?: SpanOptions): Span { - return this._tracer - .startSpan(name, options) - .setAttribute(Attributes.COMPONENT, this.component); - } -} diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 6ec0716a78..efa88318c9 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -22,7 +22,7 @@ import { IncomingHttpHeaders, } from 'http'; import { IgnoreMatcher, ParsedRequestOptions } from './types'; -import { Attributes } from './enums/attributes'; +import { AttributeNames } from './enums/attributeNames'; import * as url from 'url'; /** @@ -143,9 +143,9 @@ export class Utils { static setSpanOnError(span: Span, obj: IncomingMessage | ClientRequest) { obj.on('error', error => { span.setAttributes({ - [Attributes.ERROR]: true, - [Attributes.HTTP_ERROR_NAME]: error.name, - [Attributes.HTTP_ERROR_MESSAGE]: error.message, + [AttributeNames.ERROR]: true, + [AttributeNames.HTTP_ERROR_NAME]: error.name, + [AttributeNames.HTTP_ERROR_MESSAGE]: error.message, }); let status: Status; diff --git a/packages/opentelemetry-plugin-http/test/fixtures/google.json b/packages/opentelemetry-plugin-http/test/fixtures/google.json index 98ec958a2f..62301ab56b 100644 --- a/packages/opentelemetry-plugin-http/test/fixtures/google.json +++ b/packages/opentelemetry-plugin-http/test/fixtures/google.json @@ -40,4 +40,4 @@ ], "responseIsBinary": true } -] \ No newline at end of file +] diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts index f4f32cadc4..7df386e6f6 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -17,10 +17,11 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as url from 'url'; -import { CanonicalCode } from '@opentelemetry/types'; +import { CanonicalCode, Attributes } from '@opentelemetry/types'; import { RequestOptions } from 'https'; import { IgnoreMatcher } from '../src/types'; import { Utils } from '../src/utils'; +import * as http from 'http'; describe('Utils', () => { describe('parseResponseStatus()', () => { @@ -181,4 +182,42 @@ describe('Utils', () => { assert.strictEqual(answer2, false); }); }); + + describe('getUrlFromIncomingRequest()', () => { + it('should return absolute url with localhost', () => { + const path = '/test/1'; + const result = Utils.getUrlFromIncomingRequest(url.parse(path), {}); + assert.strictEqual(result, `http://localhost${path}`); + }); + it('should return absolute url', () => { + const absUrl = 'http://www.google/test/1?query=1'; + const result = Utils.getUrlFromIncomingRequest(url.parse(absUrl), {}); + assert.strictEqual(result, absUrl); + }); + it('should return default url', () => { + const result = Utils.getUrlFromIncomingRequest(null, {}); + assert.strictEqual(result, 'http://localhost/'); + }); + }); + describe('setSpanOnError()', () => { + it('should call span methods when we get an error event', done => { + /* tslint:disable-next-line:no-any */ + const span: any = { + setAttributes: (obj: Attributes) => {}, + setStatus: (status: unknown) => {}, + end: () => {}, + }; + sinon.spy(span, 'setAttributes'); + sinon.spy(span, 'setStatus'); + sinon.spy(span, 'end'); + const req = http.get('http://noop'); + Utils.setSpanOnError(span, req); + req.on('error', () => { + assert.strictEqual(span.setAttributes.callCount, 1); + assert.strictEqual(span.setStatus.callCount, 1); + assert.strictEqual(span.end.callCount, 1); + done(); + }); + }); + }); }); diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index cffc62f494..d47c440124 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -1,3 +1,18 @@ +/** + * Copyright 2019, 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 { SpanContext, HttpTextFormat } from '@opentelemetry/types'; import * as http from 'http'; diff --git a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts index af34f5b50c..2d04eb1d2d 100644 --- a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts +++ b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 { SpanAuditProcessor } from './SpanAuditProcessor'; import { Span, @@ -7,6 +23,8 @@ import { HttpTextFormat, } from '@opentelemetry/types'; +// TODO: remove these once we have exporter feature +// https://github.com/open-telemetry/opentelemetry-js/pull/149 export class ProxyTracer implements Tracer { private readonly _tracer: Tracer; constructor(tracer: Tracer, public audit: SpanAuditProcessor) { diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts index bdc2dca1fb..849421b573 100644 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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, Status, @@ -7,7 +23,6 @@ import { Event, } from '@opentelemetry/types'; -// } export interface SpanAudit { spanContext: SpanContext; attributes: Attributes; diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts index 53749a2d7b..a07ab892d2 100644 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 { Span } from '@opentelemetry/types'; import { SpanAudit } from './SpanAudit'; diff --git a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts index 3eb60f2ce0..4f9dbb2204 100644 --- a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts @@ -1,7 +1,23 @@ +/** + * Copyright 2019, 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 } from '@opentelemetry/types'; import * as assert from 'assert'; import * as http from 'http'; -import { Attributes } from '../../src/enums/attributes'; +import { AttributeNames } from '../../src/enums/attributeNames'; import { HttpPlugin } from '../../src/http'; import { Utils } from '../../src/utils'; import { DummyPropagation } from './DummyPropagation'; @@ -28,27 +44,27 @@ export const assertSpan = ( `${validations.httpMethod} ${validations.pathname}` ); assert.strictEqual( - span.attributes[Attributes.COMPONENT], + span.attributes[AttributeNames.COMPONENT], HttpPlugin.component ); assert.strictEqual( - span.attributes[Attributes.HTTP_ERROR_MESSAGE], + span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], span.status.message ); assert.strictEqual( - span.attributes[Attributes.HTTP_HOSTNAME], + span.attributes[AttributeNames.HTTP_HOSTNAME], validations.hostname ); assert.strictEqual( - span.attributes[Attributes.HTTP_METHOD], + span.attributes[AttributeNames.HTTP_METHOD], validations.httpMethod ); assert.strictEqual( - span.attributes[Attributes.HTTP_PATH], + span.attributes[AttributeNames.HTTP_PATH], validations.path || validations.pathname ); assert.strictEqual( - span.attributes[Attributes.HTTP_STATUS_CODE], + span.attributes[AttributeNames.HTTP_STATUS_CODE], validations.httpStatusCode ); assert.strictEqual(span.ended, true); @@ -66,7 +82,7 @@ export const assertSpan = ( const userAgent = validations.reqHeaders['user-agent']; if (userAgent) { assert.strictEqual( - span.attributes[Attributes.HTTP_USER_AGENT], + span.attributes[AttributeNames.HTTP_USER_AGENT], userAgent ); } diff --git a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts index 46cd74fe95..88d41c2e6d 100644 --- a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts +++ b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2019, 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 * as http from 'http'; import * as url from 'url'; import { RequestOptions } from 'https'; From 386a5e0788247ed4b1b44b40b6144cb3ea08f43d Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Fri, 16 Aug 2019 14:06:47 -0400 Subject: [PATCH 11/18] fix: rebase and remove/replace wrapEmitter to bind Signed-off-by: Olivier Albertini --- .../src/BasicTracer.ts | 9 ----- .../src/trace/NoopTracer.ts | 3 -- .../src/trace/TracerDelegate.ts | 8 ---- .../test/NodeTracer.test.ts | 37 ------------------- .../opentelemetry-plugin-http/src/http.ts | 8 ++-- .../test/utils/ProxyTracer.ts | 6 +-- .../opentelemetry-types/src/trace/tracer.ts | 12 ------ 7 files changed, 7 insertions(+), 76 deletions(-) diff --git a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts index 85462bf480..afa09ada98 100644 --- a/packages/opentelemetry-basic-tracer/src/BasicTracer.ts +++ b/packages/opentelemetry-basic-tracer/src/BasicTracer.ts @@ -125,15 +125,6 @@ export class BasicTracer implements types.Tracer { // Set given span to context. return this._scopeManager.with(span, fn); } - /** - * Binds the trace context to the given event emitter - */ - wrapEmitter(emitter: NodeJS.EventEmitter): void { - if (!this._scopeManager.active()) { - return; - } - this._scopeManager.bind(emitter); - } /** * Bind a span (or the current one) to the target's scope diff --git a/packages/opentelemetry-core/src/trace/NoopTracer.ts b/packages/opentelemetry-core/src/trace/NoopTracer.ts index 3f9b36306d..d10c5f6fc3 100644 --- a/packages/opentelemetry-core/src/trace/NoopTracer.ts +++ b/packages/opentelemetry-core/src/trace/NoopTracer.ts @@ -61,7 +61,4 @@ export class NoopTracer implements Tracer { getHttpTextFormat(): HttpTextFormat { return NOOP_HTTP_TEXT_FORMAT; } - - // By default does nothing - wrapEmitter(emitter: unknown): void {} } diff --git a/packages/opentelemetry-core/src/trace/TracerDelegate.ts b/packages/opentelemetry-core/src/trace/TracerDelegate.ts index 607e7d6888..0fe563a0ab 100644 --- a/packages/opentelemetry-core/src/trace/TracerDelegate.ts +++ b/packages/opentelemetry-core/src/trace/TracerDelegate.ts @@ -82,14 +82,6 @@ export class TracerDelegate implements types.Tracer { ); } - wrapEmitter(emitter: unknown): void { - this._currentTracer.wrapEmitter.apply( - this._currentTracer, - // tslint:disable-next-line:no-any - arguments as any - ); - } - recordSpanData(span: types.Span): void { return this._currentTracer.recordSpanData.apply( this._currentTracer, diff --git a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts index 2bbbd22152..c84fa6a5ab 100644 --- a/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts +++ b/packages/opentelemetry-node-tracer/test/NodeTracer.test.ts @@ -25,7 +25,6 @@ import { } from '@opentelemetry/core'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { NodeTracer } from '../src/NodeTracer'; -import { EventEmitter } from 'events'; describe('NodeTracer', () => { describe('constructor', () => { @@ -196,40 +195,4 @@ describe('NodeTracer', () => { assert.ok(tracer.getHttpTextFormat() instanceof HttpTraceContext); }); }); - describe('.wrapEmitter()', () => { - it('should not throw', () => { - const tracer = new NodeTracer({ - scopeManager: new AsyncHooksScopeManager(), - }); - tracer.wrapEmitter({} as EventEmitter); - }); - // TODO: uncomment once https://github.com/open-telemetry/opentelemetry-js/pull/146 is merged - // it('should get current Span', (done) => { - // const tracer = new NodeTracer({ - // scopeManager: new AsyncHooksScopeManager(), - // }); - - // const span = tracer.startSpan('my-span'); - // class FakeEventEmitter extends EventEmitter { - // constructor() { - // super() - // } - // test() { - // this.emit('event'); - // } - // } - - // tracer.withSpan(span, () => { - // const fake = new FakeEventEmitter(); - // tracer.wrapEmitter(fake); - // fake.on('event', () => { - // setTimeout(() => { - // assert.deepStrictEqual(tracer.getCurrentSpan(), span); - // done(); - // }, 100); - // }); - // fake.test(); - // }); - // }); - }); }); diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index b8c264bbf2..fddce1f833 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -182,7 +182,7 @@ export class HttpPlugin extends BasePlugin { propagation.inject(span.context(), Format.HTTP, opts.headers); request.on('response', (response: IncomingMessage) => { - this._tracer.wrapEmitter(response); + this._tracer.bind(response); this._logger.debug('outgoingRequest on response()'); response.on('end', () => { this._logger.debug('outgoingRequest on end()'); @@ -277,8 +277,8 @@ export class HttpPlugin extends BasePlugin { const span = plugin._startHttpSpan(`${method} ${pathname}`, spanOptions); return plugin._tracer.withSpan(span, () => { - plugin._tracer.wrapEmitter(request); - plugin._tracer.wrapEmitter(response); + plugin._tracer.bind(request); + plugin._tracer.bind(response); // Wraps end (inspired by: // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75) @@ -367,7 +367,7 @@ export class HttpPlugin extends BasePlugin { } plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); - plugin._tracer.wrapEmitter(request); + plugin._tracer.bind(request); const operationName = `${method} ${pathname}`; const spanOptions = { diff --git a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts index 2d04eb1d2d..25a203c0b4 100644 --- a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts +++ b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts @@ -30,7 +30,7 @@ export class ProxyTracer implements Tracer { constructor(tracer: Tracer, public audit: SpanAuditProcessor) { this._tracer = tracer; } - getCurrentSpan(): Span { + getCurrentSpan(): Span | null { return this._tracer.getCurrentSpan(); } startSpan(name: string, options?: SpanOptions | undefined): Span { @@ -51,7 +51,7 @@ export class ProxyTracer implements Tracer { getHttpTextFormat(): HttpTextFormat { return this._tracer.getHttpTextFormat(); } - wrapEmitter(emitter: unknown): void { - this._tracer.wrapEmitter(emitter); + bind(target: T, span?: Span | undefined): T { + return this._tracer.bind(target); } } diff --git a/packages/opentelemetry-types/src/trace/tracer.ts b/packages/opentelemetry-types/src/trace/tracer.ts index ec5b0b6106..5c980c7d85 100644 --- a/packages/opentelemetry-types/src/trace/tracer.ts +++ b/packages/opentelemetry-types/src/trace/tracer.ts @@ -96,16 +96,4 @@ export interface Tracer { * W3C Trace Context. */ getHttpTextFormat(): HttpTextFormat; - - /** - * Binds the trace context to the given event emitter. - * This is necessary in order to create child spans correctly in event - * handlers. - * @todo: Pending API discussion. Revisit if we should simply expose scopeManager instead of exposing methods through the tracer. - * Also Should we replace unknown by EventEmitter ? - * ref: https://github.com/Gozala/events - * @param emitter An event emitter whose handlers should have - * the trace context binded to them. - */ - wrapEmitter(emitter: unknown): void; } From 0583832967f1d604b66600234a2e8c826657961b Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Mon, 26 Aug 2019 19:31:20 -0400 Subject: [PATCH 12/18] fix: add Flarna recommendations fix: add Flarna recommendations fix: tests fix: OC bugs in OT only test: add assertions Allow options object as second argument Signed-off-by: Olivier Albertini --- .../opentelemetry-plugin-http/src/http.ts | 189 ++++++++++-------- .../opentelemetry-plugin-http/src/types.ts | 13 +- .../opentelemetry-plugin-http/src/utils.ts | 35 ++-- .../test/http-enable.test.ts | 54 ++++- .../test/http-package.test.ts | 28 ++- .../test/utils.test.ts | 42 ++-- .../test/utils/DummyPropagation.ts | 11 +- .../test/utils/httpRequest.ts | 7 +- 8 files changed, 240 insertions(+), 139 deletions(-) diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index fddce1f833..b2247f182c 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -40,6 +40,7 @@ import { ResponseEndArgs, ParsedRequestOptions, HttpRequestArgs, + HeaderSetter, } from './types'; import { Format } from './enums/format'; import { AttributeNames } from './enums/attributeNames'; @@ -54,11 +55,14 @@ export class HttpPlugin extends BasePlugin { protected _logger!: Logger; protected readonly _tracer!: Tracer; - constructor(public moduleName: string, public version: string) { + constructor(readonly moduleName: string, readonly version: string) { super(); // TODO: remove this once a logger will be passed // https://github.com/open-telemetry/opentelemetry-js/issues/193 this._logger = new NoopLogger(); + // TODO: remove this once options will be passed + // see https://github.com/open-telemetry/opentelemetry-js/issues/210 + this.options = {}; } /** Patches HTTP incoming and outcoming request functions. */ @@ -176,56 +180,52 @@ export class HttpPlugin extends BasePlugin { return (): ClientRequest => { this._logger.debug('makeRequestTrace by injecting context into header'); - const propagation = this._tracer.getHttpTextFormat(); - const opts = Utils.getIncomingOptions(options); - - propagation.inject(span.context(), Format.HTTP, opts.headers); - - request.on('response', (response: IncomingMessage) => { - this._tracer.bind(response); - this._logger.debug('outgoingRequest on response()'); - response.on('end', () => { - this._logger.debug('outgoingRequest on end()'); - - // TODO: create utils methods - const method = response.method - ? response.method.toUpperCase() - : 'GET'; - const headers = opts.headers; - const userAgent = headers - ? headers['user-agent'] || headers['User-Agent'] - : null; - - const host = opts.hostname || opts.host || 'localhost'; - - const attributes: Attributes = { - [AttributeNames.HTTP_URL]: `${opts.protocol}//${opts.hostname}${opts.path}`, - [AttributeNames.HTTP_HOSTNAME]: host, - [AttributeNames.HTTP_METHOD]: method, - [AttributeNames.HTTP_PATH]: opts.path || '/', - }; - - if (userAgent) { - attributes[AttributeNames.HTTP_USER_AGENT] = userAgent; - } - - if (response.statusCode) { - attributes[AttributeNames.HTTP_STATUS_CODE] = response.statusCode; - attributes[AttributeNames.HTTP_STATUS_TEXT] = - response.statusMessage; - span.setStatus(Utils.parseResponseStatus(response.statusCode)); - } - - span.setAttributes(attributes); - - if (this.options.applyCustomAttributesOnSpan) { - this.options.applyCustomAttributesOnSpan(span, request, response); - } - - span.end(); - }); - Utils.setSpanOnError(span, response); - }); + request.on( + 'response', + (response: IncomingMessage & { req?: { method?: string } }) => { + this._tracer.bind(response); + this._logger.debug('outgoingRequest on response()'); + response.on('end', () => { + this._logger.debug('outgoingRequest on end()'); + + const method = + response.req && response.req.method + ? response.req.method.toUpperCase() + : 'GET'; + const headers = options.headers; + const userAgent = headers ? headers['user-agent'] : null; + + const host = options.hostname || options.host || 'localhost'; + + const attributes: Attributes = { + [AttributeNames.HTTP_URL]: `${options.protocol}//${options.hostname}${options.path}`, + [AttributeNames.HTTP_HOSTNAME]: host, + [AttributeNames.HTTP_METHOD]: method, + [AttributeNames.HTTP_PATH]: options.path || '/', + }; + + if (userAgent) { + attributes[AttributeNames.HTTP_USER_AGENT] = userAgent; + } + + if (response.statusCode) { + attributes[AttributeNames.HTTP_STATUS_CODE] = response.statusCode; + attributes[AttributeNames.HTTP_STATUS_TEXT] = + response.statusMessage; + span.setStatus(Utils.parseResponseStatus(response.statusCode)); + } + + span.setAttributes(attributes); + + if (this.options.applyCustomAttributesOnSpan) { + this.options.applyCustomAttributesOnSpan(span, request, response); + } + + span.end(); + }); + Utils.setSpanOnError(span, response); + } + ); Utils.setSpanOnError(span, request); @@ -257,9 +257,7 @@ export class HttpPlugin extends BasePlugin { plugin._logger.debug('%s plugin incomingRequest', plugin.moduleName); - if ( - Utils.isIgnored(pathname, request, plugin.options.ignoreIncomingPaths) - ) { + if (Utils.isIgnored(pathname, plugin.options.ignoreIncomingPaths)) { return original.apply(this, [event, ...args]); } @@ -296,8 +294,7 @@ export class HttpPlugin extends BasePlugin { const hostname = headers.host ? headers.host.replace(/^(.*)(\:[0-9]{1,5})/, '$1') : 'localhost'; - const userAgent = (headers['user-agent'] || - headers['User-Agent']) as string; + const userAgent = headers['user-agent'] as string; const attributes: Attributes = { [AttributeNames.HTTP_URL]: Utils.getUrlFromIncomingRequest( @@ -349,53 +346,79 @@ export class HttpPlugin extends BasePlugin { } const { origin, pathname, method, optionsParsed } = Utils.getRequestInfo( - options + options, + typeof args[0] === 'object' && typeof options === 'string' + ? (args.shift() as RequestOptions) + : undefined ); - const request: ClientRequest = original.apply(this, [ - optionsParsed, - ...args, - ]); + options = optionsParsed; if ( - Utils.isIgnored( - origin + pathname, - request, - plugin.options.ignoreOutgoingUrls - ) + Utils.isIgnored(origin + pathname, plugin.options.ignoreOutgoingUrls) ) { - return request; + return original.apply(this, [options, ...args]); } - plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); - plugin._tracer.bind(request); - + const currentSpan = plugin._tracer.getCurrentSpan(); const operationName = `${method} ${pathname}`; - const spanOptions = { + const spanOptions: SpanOptions = { kind: SpanKind.CLIENT, + parent: currentSpan ? currentSpan : undefined, }; - const currentSpan = plugin._tracer.getCurrentSpan(); + + const span = plugin._startHttpSpan(operationName, spanOptions); + + const propagation = plugin._tracer.getHttpTextFormat(); + let setter: HeaderSetter; + const hasExpectHeader = Utils.hasExpectHeader(options as RequestOptions); + + if (hasExpectHeader) { + setter = { + setHeader(name: string, value: string) { + // If outgoing request headers contain the "Expect" header, the + // returned ClientRequest will throw an error if any new headers are + // added. We need to set the header directly in the headers object + // which has been cloned earlier. + (options as RequestOptions).headers![name] = value; + }, + }; + // If outgoing request headers contain the "Expect" header + // We must propagate before headers are sent + // which is the case when "Expect" header is present and original.apply is called + propagation.inject(span.context(), Format.HTTP, setter); + } + + const request: ClientRequest = original.apply(this, [options, ...args]); + + if (!hasExpectHeader) { + setter = { + setHeader(name: string, value: string) { + // The returned ClientRequest will throw an error if any new headers are + // added when headers are already sent + if (!request.headersSent) { + request.setHeader(name, value); + } + }, + }; + propagation.inject(span.context(), Format.HTTP, setter); + } + + plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); + plugin._tracer.bind(request); + // Checks if this outgoing request is part of an operation by checking // if there is a current span, if so, we create a child span. In // case there is no active span, this means that the outgoing request is // the first operation, therefore we create a span and call withSpan method. if (!currentSpan) { plugin._logger.debug('outgoingRequest starting a span without context'); - const span = plugin._startHttpSpan(operationName, spanOptions); return plugin._tracer.withSpan( span, - plugin._getMakeRequestTraceFunction(request, optionsParsed, span) + plugin._getMakeRequestTraceFunction(request, options, span) ); } else { plugin._logger.debug('outgoingRequest starting a child span'); - const span = plugin._startHttpSpan(operationName, { - kind: spanOptions.kind, - parent: currentSpan, - }); - return plugin._getMakeRequestTraceFunction( - request, - optionsParsed, - span - )(); + return plugin._getMakeRequestTraceFunction(request, options, span)(); } }; } diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index 6322508dd1..cd6582fd88 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -25,14 +25,15 @@ import { } from 'http'; import * as http from 'http'; -export type IgnoreMatcher = - | string - | RegExp - | ((url: string, request: T) => boolean); +export type IgnoreMatcher = string | RegExp | ((url: string) => boolean); export type HttpCallback = (res: IncomingMessage) => void; export type RequestFunction = typeof request; export type GetFunction = typeof get; +export interface HeaderSetter { + setHeader: (name: string, value: string) => void; +} + export type HttpCallbackOptional = HttpCallback | undefined; // from node 10+ @@ -61,7 +62,7 @@ export interface HttpCustomAttributeFunction { } export interface HttpPluginConfig { - ignoreIncomingPaths?: Array>; - ignoreOutgoingUrls?: Array>; + ignoreIncomingPaths?: IgnoreMatcher[]; + ignoreOutgoingUrls?: IgnoreMatcher[]; applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; } diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index efa88318c9..877c723d74 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -84,7 +84,8 @@ export class Utils { static hasExpectHeader(options: RequestOptions | url.URL): boolean { return !!( (options as RequestOptions).headers && - (options as RequestOptions).headers!.Expect + ((options as RequestOptions).headers!.Expect || + (options as RequestOptions).headers!.expect) ); } @@ -96,15 +97,14 @@ export class Utils { */ static satisfiesPattern( constant: string, - obj: T, - pattern: IgnoreMatcher + pattern: IgnoreMatcher ): boolean { if (typeof pattern === 'string') { return pattern === constant; } else if (pattern instanceof RegExp) { return pattern.test(constant); } else if (typeof pattern === 'function') { - return pattern(constant, obj); + return pattern(constant); } else { throw new TypeError('Pattern is in unsupported datatype'); } @@ -116,18 +116,14 @@ export class Utils { * @param obj obj to inspect * @param list List of ignore patterns */ - static isIgnored( - constant: string, - obj: T, - list?: Array> - ): boolean { + static isIgnored(constant: string, list?: IgnoreMatcher[]): boolean { if (!list) { // No ignored urls - trace everything return false; } for (const pattern of list) { - if (Utils.satisfiesPattern(constant, obj, pattern)) { + if (Utils.satisfiesPattern(constant, pattern)) { return true; } } @@ -169,7 +165,10 @@ export class Utils { * return an object with default value and parsed options * @param options for the request */ - static getRequestInfo(options: RequestOptions | string) { + static getRequestInfo( + options: RequestOptions | string, + extraOptions?: RequestOptions + ) { let pathname = '/'; let origin = ''; let optionsParsed: url.URL | url.UrlWithStringQuery | RequestOptions; @@ -177,6 +176,9 @@ export class Utils { optionsParsed = url.parse(options); pathname = (optionsParsed as url.UrlWithStringQuery).pathname || '/'; origin = `${optionsParsed.protocol || 'http:'}//${optionsParsed.host}`; + if (extraOptions !== undefined) { + Object.assign(optionsParsed, extraOptions); + } } else { optionsParsed = options; try { @@ -184,9 +186,16 @@ export class Utils { if (!pathname && options.path) { pathname = url.parse(options.path).pathname || '/'; } - origin = `${options.protocol || 'http:'}//${options.host}`; + origin = `${options.protocol || 'http:'}//${options.host || + `${options.hostname}:${options.port}`}`; } catch (ignore) {} } + if (Utils.hasExpectHeader(optionsParsed)) { + (optionsParsed as RequestOptions).headers = Object.assign( + {}, + (optionsParsed as RequestOptions).headers + ); + } // some packages return method in lowercase.. // ensure upperCase for consistency let method = (optionsParsed as RequestOptions).method; @@ -195,7 +204,7 @@ export class Utils { return { origin, pathname, method, optionsParsed }; } - static getIncomingOptions(options: ParsedRequestOptions) { + static getOutgoingOptions(options: ParsedRequestOptions) { // If outgoing request headers contain the "Expect" header, the returned // ClientRequest will throw an error if any new headers are added. // So we need to clone the options object to be able to inject new diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/http-enable.test.ts index a5613b874d..7a4232a389 100644 --- a/packages/opentelemetry-plugin-http/test/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-enable.test.ts @@ -124,6 +124,8 @@ describe('HttpPlugin', () => { }; assert.strictEqual(spans.length, 2); + assert.ok(result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); + assert.ok(result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); assertSpan(incomingSpan, SpanKind.SERVER, validations); assertSpan(outgoingSpan, SpanKind.CLIENT, validations); done(); @@ -342,15 +344,65 @@ describe('HttpPlugin', () => { const span = spans[0]; const validations = { hostname: 'google.fr', - httpStatusCode: result.statusCode!, + httpStatusCode: 301, httpMethod: 'GET', pathname: '/', resHeaders: result.resHeaders, reqHeaders: result.reqHeaders, }; + assert.ok(result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); + assert.ok(result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); assertSpan(span, SpanKind.CLIENT, validations); }); nock.disableNetConnect(); }); + 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: http.get(url, options, callback) and following headers: ${JSON.stringify( + headers + )}`, done => { + nock.enableNetConnect(); + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 0); + const options = { headers }; + http.get('http://google.fr/', options, (resp: http.IncomingMessage) => { + const res = (resp as unknown) as http.IncomingMessage & { + req: http.IncomingMessage; + }; + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + const validations = { + hostname: 'google.fr', + 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 */ + }; + assert.ok(data); + assert.ok( + validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] + ); + assert.ok( + validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] + ); + done(); + }); + }); + nock.disableNetConnect(); + }); + } }); }); diff --git a/packages/opentelemetry-plugin-http/test/http-package.test.ts b/packages/opentelemetry-plugin-http/test/http-package.test.ts index 5bfa261a88..3ab2d8c48e 100644 --- a/packages/opentelemetry-plugin-http/test/http-package.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-package.test.ts @@ -67,10 +67,10 @@ describe('Packages', () => { [ { name: 'axios', httpPackage: axios }, //keep first { name: 'superagent', httpPackage: superagent }, - { name: 'got', httpPackage: { get: async (url: string) => got(url) } }, + { name: 'got', httpPackage: { get: (url: string) => got(url) } }, { name: 'request', - httpPackage: { get: async (url: string) => request(url) }, + httpPackage: { get: (url: string) => request(url) }, }, ].forEach(({ name, httpPackage }) => { it(`should create a span for GET requests and add propagation headers by using ${name} package`, async () => { @@ -85,7 +85,13 @@ describe('Packages', () => { } const urlparsed = url.parse( - `http://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8` + name === 'got' && process.versions.node.startsWith('12') + ? // there is an issue with got 9.6 version and node 12 when redirecting so url above will not work + // https://github.com/nock/nock/pull/1551 + // https://github.com/sindresorhus/got/commit/bf1aa5492ae2bc78cbbec6b7d764906fb156e6c2#diff-707a4781d57c42085155dcb27edb9ccbR258 + // TODO: check if this is still the case when new version + 'http://info.cern.ch/' + : `http://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8` ); const result = await httpPackage.get(urlparsed.href!); if (!resHeaders) { @@ -105,6 +111,22 @@ describe('Packages', () => { path: urlparsed.path, resHeaders, }; + + switch (name) { + case 'axios': + assert.ok( + result.request._headers[DummyPropagation.TRACE_CONTEXT_KEY] + ); + assert.ok( + result.request._headers[DummyPropagation.SPAN_CONTEXT_KEY] + ); + break; + case 'got': + case 'superagent': + break; + default: + break; + } assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); assertSpan(span, SpanKind.CLIENT, validations); }); diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts index 7df386e6f6..7a47e79996 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -18,7 +18,6 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as url from 'url'; import { CanonicalCode, Attributes } from '@opentelemetry/types'; -import { RequestOptions } from 'https'; import { IgnoreMatcher } from '../src/types'; import { Utils } from '../src/utils'; import * as http from 'http'; @@ -49,31 +48,31 @@ describe('Utils', () => { describe('hasExpectHeader()', () => { it('should throw if no option', () => { try { - Utils.hasExpectHeader('' as RequestOptions); + Utils.hasExpectHeader('' as http.RequestOptions); assert.fail(); } catch (ignore) {} }); it('should not throw if no headers', () => { - const result = Utils.hasExpectHeader({} as RequestOptions); + const result = Utils.hasExpectHeader({} as http.RequestOptions); assert.strictEqual(result, false); }); it('should return true on Expect', () => { const result = Utils.hasExpectHeader({ headers: { Expect: 1 }, - } as RequestOptions); + } as http.RequestOptions); assert.strictEqual(result, true); }); }); - describe('getIncomingOptions()', () => { + describe('getOutgoingOptions()', () => { it('should get options object', () => { const options = Object.assign( { headers: { Expect: '100-continue' } }, url.parse('http://google.fr/') ); - const result = Utils.getIncomingOptions(options); + const result = Utils.getOutgoingOptions(options); assert.strictEqual(result.hostname, 'google.fr'); assert.strictEqual(result.headers!.Expect, options.headers.Expect); assert.strictEqual(result.protocol, 'http:'); @@ -94,26 +93,22 @@ describe('Utils', () => { describe('satisfiesPattern()', () => { it('string pattern', () => { - const answer1 = Utils.satisfiesPattern('/test/1', {}, '/test/1'); + const answer1 = Utils.satisfiesPattern('/test/1', '/test/1'); assert.strictEqual(answer1, true); - const answer2 = Utils.satisfiesPattern('/test/1', {}, '/test/11'); + const answer2 = Utils.satisfiesPattern('/test/1', '/test/11'); assert.strictEqual(answer2, false); }); it('regex pattern', () => { - const answer1 = Utils.satisfiesPattern('/TeSt/1', {}, /\/test/i); + const answer1 = Utils.satisfiesPattern('/TeSt/1', /\/test/i); assert.strictEqual(answer1, true); - const answer2 = Utils.satisfiesPattern('/2/tEst/1', {}, /\/test/); + const answer2 = Utils.satisfiesPattern('/2/tEst/1', /\/test/); assert.strictEqual(answer2, false); }); it('should throw if type is unknown', () => { try { - Utils.satisfiesPattern( - '/TeSt/1', - {}, - (true as unknown) as IgnoreMatcher<{}> - ); + Utils.satisfiesPattern('/TeSt/1', (true as unknown) as IgnoreMatcher); assert.fail(); } catch (error) { assert.strictEqual(error instanceof TypeError, true); @@ -123,15 +118,12 @@ describe('Utils', () => { it('function pattern', () => { const answer1 = Utils.satisfiesPattern( '/test/home', - { headers: {} }, - (url: string, req: { headers: unknown }) => - req.headers && url === '/test/home' + (url: string) => url === '/test/home' ); assert.strictEqual(answer1, true); const answer2 = Utils.satisfiesPattern( '/test/home', - { headers: {} }, - (url: string, req: { headers: unknown }) => url !== '/test/home' + (url: string) => url !== '/test/home' ); assert.strictEqual(answer2, false); }); @@ -147,7 +139,7 @@ describe('Utils', () => { }); it('should call isSatisfyPattern, n match', () => { - const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); + const answer1 = Utils.isIgnored('/test/1', ['/test/11']); assert.strictEqual(answer1, false); assert.strictEqual( (Utils.satisfiesPattern as sinon.SinonSpy).callCount, @@ -156,7 +148,7 @@ describe('Utils', () => { }); it('should call isSatisfyPattern, match', () => { - const answer1 = Utils.isIgnored('/test/1', {}, ['/test/11']); + const answer1 = Utils.isIgnored('/test/1', ['/test/11']); assert.strictEqual(answer1, false); assert.strictEqual( (Utils.satisfiesPattern as sinon.SinonSpy).callCount, @@ -165,7 +157,7 @@ describe('Utils', () => { }); it('should not call isSatisfyPattern', () => { - Utils.isIgnored('/test/1', {}, []); + Utils.isIgnored('/test/1', []); assert.strictEqual( (Utils.satisfiesPattern as sinon.SinonSpy).callCount, 0 @@ -173,12 +165,12 @@ describe('Utils', () => { }); it('should return false on empty list', () => { - const answer1 = Utils.isIgnored('/test/1', {}, []); + const answer1 = Utils.isIgnored('/test/1', []); assert.strictEqual(answer1, false); }); it('should not throw and return false when list is undefined', () => { - const answer2 = Utils.isIgnored('/test/1', {}, undefined); + const answer2 = Utils.isIgnored('/test/1', undefined); assert.strictEqual(answer2, false); }); }); diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index d47c440124..73c0381a7d 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -14,7 +14,8 @@ * limitations under the License. */ import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; -import * as http from 'http'; +import { HeaderSetter } from '../../src/types'; +// import * as http from 'http'; export class DummyPropagation implements HttpTextFormat { static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; @@ -28,11 +29,9 @@ export class DummyPropagation implements HttpTextFormat { inject( spanContext: SpanContext, format: string, - headers: http.IncomingHttpHeaders + headers: HeaderSetter ): void { - headers[DummyPropagation.TRACE_CONTEXT_KEY] = - spanContext.traceId || 'undefined'; - headers[DummyPropagation.SPAN_CONTEXT_KEY] = - spanContext.spanId || 'undefined'; + headers.setHeader(DummyPropagation.TRACE_CONTEXT_KEY, spanContext.traceId); + headers.setHeader(DummyPropagation.SPAN_CONTEXT_KEY, spanContext.spanId); } } diff --git a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts index 88d41c2e6d..0efbdfb3a1 100644 --- a/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts +++ b/packages/opentelemetry-plugin-http/test/utils/httpRequest.ts @@ -49,8 +49,11 @@ export const httpRequest = { resolve({ data, statusCode: res.statusCode, - /* tslint:disable-next-line:no-any */ - reqHeaders: (res.req as any)._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 */ resHeaders: res.headers, method: res.req.method, }); From 171440115a3377ee5610e54bcaededfe761be755 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Tue, 27 Aug 2019 09:19:37 -0400 Subject: [PATCH 13/18] refactor: simplify propagation usage Signed-off-by: Olivier Albertini --- .../opentelemetry-plugin-http/src/http.ts | 37 ++----------------- .../opentelemetry-plugin-http/src/types.ts | 4 -- .../opentelemetry-plugin-http/src/utils.ts | 4 ++ .../test/utils/DummyPropagation.ts | 7 ++-- 4 files changed, 10 insertions(+), 42 deletions(-) diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index b2247f182c..a42eb0af7a 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -40,7 +40,6 @@ import { ResponseEndArgs, ParsedRequestOptions, HttpRequestArgs, - HeaderSetter, } from './types'; import { Format } from './enums/format'; import { AttributeNames } from './enums/attributeNames'; @@ -367,42 +366,12 @@ export class HttpPlugin extends BasePlugin { }; const span = plugin._startHttpSpan(operationName, spanOptions); - - const propagation = plugin._tracer.getHttpTextFormat(); - let setter: HeaderSetter; - const hasExpectHeader = Utils.hasExpectHeader(options as RequestOptions); - - if (hasExpectHeader) { - setter = { - setHeader(name: string, value: string) { - // If outgoing request headers contain the "Expect" header, the - // returned ClientRequest will throw an error if any new headers are - // added. We need to set the header directly in the headers object - // which has been cloned earlier. - (options as RequestOptions).headers![name] = value; - }, - }; - // If outgoing request headers contain the "Expect" header - // We must propagate before headers are sent - // which is the case when "Expect" header is present and original.apply is called - propagation.inject(span.context(), Format.HTTP, setter); - } + plugin._tracer + .getHttpTextFormat() + .inject(span.context(), Format.HTTP, options.headers); const request: ClientRequest = original.apply(this, [options, ...args]); - if (!hasExpectHeader) { - setter = { - setHeader(name: string, value: string) { - // The returned ClientRequest will throw an error if any new headers are - // added when headers are already sent - if (!request.headersSent) { - request.setHeader(name, value); - } - }, - }; - propagation.inject(span.context(), Format.HTTP, setter); - } - plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName); plugin._tracer.bind(request); diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index cd6582fd88..e01839e142 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -30,10 +30,6 @@ export type HttpCallback = (res: IncomingMessage) => void; export type RequestFunction = typeof request; export type GetFunction = typeof get; -export interface HeaderSetter { - setHeader: (name: string, value: string) => void; -} - export type HttpCallbackOptional = HttpCallback | undefined; // from node 10+ diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 877c723d74..fc02e706d6 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -172,6 +172,7 @@ export class Utils { let pathname = '/'; let origin = ''; let optionsParsed: url.URL | url.UrlWithStringQuery | RequestOptions; + if (typeof options === 'string') { optionsParsed = url.parse(options); pathname = (optionsParsed as url.UrlWithStringQuery).pathname || '/'; @@ -190,11 +191,14 @@ export class Utils { `${options.hostname}:${options.port}`}`; } catch (ignore) {} } + if (Utils.hasExpectHeader(optionsParsed)) { (optionsParsed as RequestOptions).headers = Object.assign( {}, (optionsParsed as RequestOptions).headers ); + } else if (!(optionsParsed as RequestOptions).headers) { + (optionsParsed as RequestOptions).headers = {}; } // some packages return method in lowercase.. // ensure upperCase for consistency diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index 73c0381a7d..f9ce219fb4 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; -import { HeaderSetter } from '../../src/types'; // import * as http from 'http'; export class DummyPropagation implements HttpTextFormat { @@ -29,9 +28,9 @@ export class DummyPropagation implements HttpTextFormat { inject( spanContext: SpanContext, format: string, - headers: HeaderSetter + headers: { [custom: string]: string } ): void { - headers.setHeader(DummyPropagation.TRACE_CONTEXT_KEY, spanContext.traceId); - headers.setHeader(DummyPropagation.SPAN_CONTEXT_KEY, spanContext.spanId); + headers[DummyPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId; + headers[DummyPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId; } } From d5e6531570f55bd740cbc70da5b7476805ac6c07 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Tue, 27 Aug 2019 10:42:30 -0400 Subject: [PATCH 14/18] fix: add Flarna recommandations Signed-off-by: Olivier Albertini --- .../opentelemetry-plugin-http/src/http.ts | 2 +- .../opentelemetry-plugin-http/src/utils.ts | 30 ++++----------- .../test/http-disable.test.ts | 37 +------------------ .../test/utils.test.ts | 27 ++++---------- 4 files changed, 17 insertions(+), 79 deletions(-) diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index a42eb0af7a..e5c9a65c09 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -293,7 +293,7 @@ export class HttpPlugin extends BasePlugin { const hostname = headers.host ? headers.host.replace(/^(.*)(\:[0-9]{1,5})/, '$1') : 'localhost'; - const userAgent = headers['user-agent'] as string; + const userAgent = headers['user-agent']; const attributes: Attributes = { [AttributeNames.HTTP_URL]: Utils.getUrlFromIncomingRequest( diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index fc02e706d6..9103495b5f 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -21,7 +21,7 @@ import { ClientRequest, IncomingHttpHeaders, } from 'http'; -import { IgnoreMatcher, ParsedRequestOptions } from './types'; +import { IgnoreMatcher } from './types'; import { AttributeNames } from './enums/attributeNames'; import * as url from 'url'; @@ -82,11 +82,12 @@ export class Utils { * @param options Options for http.request. */ static hasExpectHeader(options: RequestOptions | url.URL): boolean { - return !!( - (options as RequestOptions).headers && - ((options as RequestOptions).headers!.Expect || - (options as RequestOptions).headers!.expect) - ); + if (typeof (options as RequestOptions).headers !== 'object') { + return false; + } + + const keys = Object.keys((options as RequestOptions).headers!); + return !!keys.find(key => key.toLowerCase() === 'expect'); } /** @@ -207,21 +208,4 @@ export class Utils { return { origin, pathname, method, optionsParsed }; } - - static getOutgoingOptions(options: ParsedRequestOptions) { - // If outgoing request headers contain the "Expect" header, the returned - // ClientRequest will throw an error if any new headers are added. - // So we need to clone the options object to be able to inject new - // header. - if (Utils.hasExpectHeader(options)) { - const safeOptions = Object.assign({}, options); - safeOptions.headers = Object.assign({}, options.headers); - return safeOptions; - } - - if (!options.headers) { - options.headers = {}; - } - return options; - } } diff --git a/packages/opentelemetry-plugin-http/test/http-disable.test.ts b/packages/opentelemetry-plugin-http/test/http-disable.test.ts index 74b7392d1a..0e36869409 100644 --- a/packages/opentelemetry-plugin-http/test/http-disable.test.ts +++ b/packages/opentelemetry-plugin-http/test/http-disable.test.ts @@ -20,45 +20,12 @@ import * as nock from 'nock'; import * as sinon from 'sinon'; import { plugin } from '../src/http'; -import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { NodeTracer } from '@opentelemetry/node-tracer'; import { NoopLogger } from '@opentelemetry/core'; import { AddressInfo } from 'net'; - -const httpRequest = { - get: (options: {} | string) => { - return new Promise((resolve, reject) => { - return http.get(options, resp => { - let data = ''; - resp.on('data', chunk => { - data += chunk; - }); - resp.on('end', () => { - resolve(data); - }); - resp.on('error', err => { - reject(err); - }); - }); - }); - }, -}; - -class DummyPropagation implements HttpTextFormat { - extract(headers: unknown): SpanContext { - return { traceId: 'dummy-trace-id', spanId: 'dummy-span-id' }; - } - - inject( - spanContext: SpanContext, - format: string, - headers: http.IncomingHttpHeaders - ): void { - headers['x-dummy-trace-id'] = spanContext.traceId || 'undefined'; - headers['x-dummy-span-id'] = spanContext.spanId || 'undefined'; - } -} +import { DummyPropagation } from './utils/DummyPropagation'; +import { httpRequest } from './utils/httpRequest'; describe('HttpPlugin', () => { let server: http.Server; diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/utils.test.ts index 7a47e79996..d4a602d3a0 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/utils.test.ts @@ -58,26 +58,13 @@ describe('Utils', () => { assert.strictEqual(result, false); }); - it('should return true on Expect', () => { - const result = Utils.hasExpectHeader({ - headers: { Expect: 1 }, - } as http.RequestOptions); - assert.strictEqual(result, true); - }); - }); - - describe('getOutgoingOptions()', () => { - it('should get options object', () => { - const options = Object.assign( - { headers: { Expect: '100-continue' } }, - url.parse('http://google.fr/') - ); - const result = Utils.getOutgoingOptions(options); - assert.strictEqual(result.hostname, 'google.fr'); - assert.strictEqual(result.headers!.Expect, options.headers.Expect); - assert.strictEqual(result.protocol, 'http:'); - assert.strictEqual(result.path, '/'); - assert.strictEqual((result as url.URL).pathname, '/'); + it('should return true on Expect (no case sensitive)', () => { + for (const headers of [{ Expect: 1 }, { expect: 1 }, { ExPect: 1 }]) { + const result = Utils.hasExpectHeader({ + headers, + } as http.RequestOptions); + assert.strictEqual(result, true); + } }); }); From 9e6215e0ac5e4ba0bdf52acb6e88cbe301af403b Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Tue, 27 Aug 2019 11:10:31 -0400 Subject: [PATCH 15/18] refactor(test): use ReadableSpan Signed-off-by: Olivier Albertini --- .../opentelemetry-plugin-http/package.json | 3 +- .../{ => functionals}/http-disable.test.ts | 6 +- .../{ => functionals}/http-enable.test.ts | 31 ++- .../{ => functionals}/http-package.test.ts | 29 +-- .../test/{ => functionals}/utils.test.ts | 4 +- .../test/integrations/http-enable.test.ts | 187 ++++++++++++++++++ .../test/utils/ProxyTracer.ts | 57 ------ .../test/utils/SpanAudit.ts | 38 ---- .../test/utils/SpanAuditProcessor.ts | 27 +-- .../test/utils/TracerTest.ts | 34 ++++ .../test/utils/assertSpan.ts | 9 +- 11 files changed, 267 insertions(+), 158 deletions(-) rename packages/opentelemetry-plugin-http/test/{ => functionals}/http-disable.test.ts (94%) rename packages/opentelemetry-plugin-http/test/{ => functionals}/http-enable.test.ts (94%) rename packages/opentelemetry-plugin-http/test/{ => functionals}/http-package.test.ts (89%) rename packages/opentelemetry-plugin-http/test/{ => functionals}/utils.test.ts (98%) create mode 100644 packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts delete mode 100644 packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts delete mode 100644 packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts create mode 100644 packages/opentelemetry-plugin-http/test/utils/TracerTest.ts diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index e7d7829046..a51ac99f25 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -6,7 +6,7 @@ "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", "scripts": { - "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", + "test": "nyc ts-mocha -p tsconfig.json test/**/*/*.test.ts", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", "check": "gts check", @@ -49,6 +49,7 @@ "@types/superagent": "^4.1.3", "@opentelemetry/scope-async-hooks": "^0.0.1", "@opentelemetry/scope-base": "^0.0.1", + "@opentelemetry/basic-tracer": "^0.0.1", "axios": "^0.19.0", "got": "^9.6.0", "request": "^2.88.0", diff --git a/packages/opentelemetry-plugin-http/test/http-disable.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-disable.test.ts similarity index 94% rename from packages/opentelemetry-plugin-http/test/http-disable.test.ts rename to packages/opentelemetry-plugin-http/test/functionals/http-disable.test.ts index 0e36869409..55ed7a8d0e 100644 --- a/packages/opentelemetry-plugin-http/test/http-disable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-disable.test.ts @@ -19,13 +19,13 @@ import * as http from 'http'; import * as nock from 'nock'; import * as sinon from 'sinon'; -import { plugin } from '../src/http'; +import { plugin } from '../../src/http'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { NodeTracer } from '@opentelemetry/node-tracer'; import { NoopLogger } from '@opentelemetry/core'; import { AddressInfo } from 'net'; -import { DummyPropagation } from './utils/DummyPropagation'; -import { httpRequest } from './utils/httpRequest'; +import { DummyPropagation } from '../utils/DummyPropagation'; +import { httpRequest } from '../utils/httpRequest'; describe('HttpPlugin', () => { let server: http.Server; diff --git a/packages/opentelemetry-plugin-http/test/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts similarity index 94% rename from packages/opentelemetry-plugin-http/test/http-enable.test.ts rename to packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts index 7a4232a389..8037ded424 100644 --- a/packages/opentelemetry-plugin-http/test/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts @@ -15,18 +15,17 @@ */ import { NoopLogger } from '@opentelemetry/core'; -import { NodeTracer } from '@opentelemetry/node-tracer'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { SpanKind, Span } from '@opentelemetry/types'; import * as assert from 'assert'; import * as http from 'http'; import * as nock from 'nock'; -import { HttpPlugin, plugin } from '../src/http'; -import { assertSpan } from './utils/assertSpan'; -import { DummyPropagation } from './utils/DummyPropagation'; -import { httpRequest } from './utils/httpRequest'; -import { ProxyTracer } from './utils/ProxyTracer'; -import { SpanAuditProcessor } from './utils/SpanAuditProcessor'; +import { HttpPlugin, plugin } from '../../src/http'; +import { assertSpan } from '../utils/assertSpan'; +import { DummyPropagation } from '../utils/DummyPropagation'; +import { httpRequest } from '../utils/httpRequest'; +import { TracerTest } from '../utils/TracerTest'; +import { SpanAuditProcessor } from '../utils/SpanAuditProcessor'; import * as url from 'url'; let server: http.Server; @@ -71,12 +70,14 @@ describe('HttpPlugin', () => { const scopeManager = new AsyncHooksScopeManager(); const httpTextFormat = new DummyPropagation(); const logger = new NoopLogger(); - const realTracer = new NodeTracer({ - scopeManager, - logger, - httpTextFormat, - }); - const tracer = new ProxyTracer(realTracer, audit); + const tracer = new TracerTest( + { + scopeManager, + logger, + httpTextFormat, + }, + audit + ); beforeEach(() => { audit.reset(); }); @@ -124,8 +125,6 @@ describe('HttpPlugin', () => { }; assert.strictEqual(spans.length, 2); - assert.ok(result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); - assert.ok(result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); assertSpan(incomingSpan, SpanKind.SERVER, validations); assertSpan(outgoingSpan, SpanKind.CLIENT, validations); done(); @@ -350,8 +349,6 @@ describe('HttpPlugin', () => { resHeaders: result.resHeaders, reqHeaders: result.reqHeaders, }; - assert.ok(result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); - assert.ok(result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); assertSpan(span, SpanKind.CLIENT, validations); }); nock.disableNetConnect(); diff --git a/packages/opentelemetry-plugin-http/test/http-package.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts similarity index 89% rename from packages/opentelemetry-plugin-http/test/http-package.test.ts rename to packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts index 3ab2d8c48e..d8e4d16810 100644 --- a/packages/opentelemetry-plugin-http/test/http-package.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts @@ -15,22 +15,22 @@ */ import { NoopLogger } from '@opentelemetry/core'; -import { NodeTracer } from '@opentelemetry/node-tracer'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; import { SpanKind, Span } from '@opentelemetry/types'; import * as assert from 'assert'; import * as http from 'http'; import * as nock from 'nock'; -import { plugin } from '../src/http'; -import { assertSpan } from './utils/assertSpan'; -import { DummyPropagation } from './utils/DummyPropagation'; -import { ProxyTracer } from './utils/ProxyTracer'; -import { SpanAuditProcessor } from './utils/SpanAuditProcessor'; +import { plugin } from '../../src/http'; +import { assertSpan } from '../utils/assertSpan'; +import { DummyPropagation } from '../utils/DummyPropagation'; +import { TracerTest } from '../utils/TracerTest'; +import { SpanAuditProcessor } from '../utils/SpanAuditProcessor'; import * as url from 'url'; import axios, { AxiosResponse } from 'axios'; import * as superagent from 'superagent'; import * as got from 'got'; import * as request from 'request-promise-native'; +import * as path from 'path'; const audit = new SpanAuditProcessor(); @@ -43,12 +43,15 @@ describe('Packages', () => { const scopeManager = new AsyncHooksScopeManager(); const httpTextFormat = new DummyPropagation(); const logger = new NoopLogger(); - const realTracer = new NodeTracer({ - scopeManager, - logger, - httpTextFormat, - }); - const tracer = new ProxyTracer(realTracer, audit); + + const tracer = new TracerTest( + { + scopeManager, + logger, + httpTextFormat, + }, + audit + ); beforeEach(() => { audit.reset(); }); @@ -81,7 +84,7 @@ describe('Packages', () => { nock.cleanAll(); nock.enableNetConnect(); } else { - nock.load(__dirname + '/fixtures/google.json'); + nock.load(path.join(__dirname, '../', '/fixtures/google.json')); } const urlparsed = url.parse( diff --git a/packages/opentelemetry-plugin-http/test/utils.test.ts b/packages/opentelemetry-plugin-http/test/functionals/utils.test.ts similarity index 98% rename from packages/opentelemetry-plugin-http/test/utils.test.ts rename to packages/opentelemetry-plugin-http/test/functionals/utils.test.ts index d4a602d3a0..232f1ec555 100644 --- a/packages/opentelemetry-plugin-http/test/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/utils.test.ts @@ -18,8 +18,8 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as url from 'url'; import { CanonicalCode, Attributes } from '@opentelemetry/types'; -import { IgnoreMatcher } from '../src/types'; -import { Utils } from '../src/utils'; +import { IgnoreMatcher } from '../../src/types'; +import { Utils } from '../../src/utils'; import * as http from 'http'; describe('Utils', () => { diff --git a/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts new file mode 100644 index 0000000000..b5280bdc8d --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts @@ -0,0 +1,187 @@ +/** + * Copyright 2019, 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 { NoopLogger } from '@opentelemetry/core'; +import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; +import { SpanKind, Span } from '@opentelemetry/types'; +import * as assert from 'assert'; +import * as http from 'http'; +import { plugin } from '../../src/http'; +import { assertSpan } from '../utils/assertSpan'; +import { DummyPropagation } from '../utils/DummyPropagation'; +import { httpRequest } from '../utils/httpRequest'; +import { TracerTest } from '../utils/TracerTest'; +import { SpanAuditProcessor } from '../utils/SpanAuditProcessor'; +import * as url from 'url'; + +const serverPort = 12345; +const hostname = 'localhost'; +const audit = new SpanAuditProcessor(); + +export const customAttributeFunction = (span: Span): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +describe('HttpPlugin Integration tests', () => { + describe('enable()', () => { + const scopeManager = new AsyncHooksScopeManager(); + const httpTextFormat = new DummyPropagation(); + const logger = new NoopLogger(); + const tracer = new TracerTest( + { + scopeManager, + logger, + httpTextFormat, + }, + audit + ); + beforeEach(() => { + audit.reset(); + }); + + before(() => { + plugin.disable(); + plugin.enable(http, tracer); + const ignoreConfig = [ + `http://${hostname}:${serverPort}/ignored/string`, + /\/ignored\/regexp$/i, + (url: string) => url.endsWith(`/ignored/function`), + ]; + plugin.options = { + ignoreIncomingPaths: ignoreConfig, + ignoreOutgoingUrls: ignoreConfig, + applyCustomAttributesOnSpan: customAttributeFunction, + }; + }); + + after(() => { + plugin.disable(); + }); + + it('should create a rootSpan for GET requests and add propagation headers', async () => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 0); + await httpRequest.get(`http://google.fr/?query=test`).then(result => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + assertSpan(span, SpanKind.CLIENT, validations); + }); + }); + + it('custom attributes should show up on client spans', async () => { + await httpRequest.get(`http://google.fr/`).then(result => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); + assertSpan(span, SpanKind.CLIENT, validations); + }); + }); + + it('should create a span for GET requests and add propagation headers with Expect headers', async () => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 0); + const options = Object.assign( + { headers: { Expect: '100-continue' } }, + url.parse('http://google.fr/') + ); + await httpRequest.get(options).then(result => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: 301, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + }; + 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: http.get(url, options, callback) and following headers: ${JSON.stringify( + headers + )}`, done => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 0); + const options = { headers }; + http.get('http://google.fr/', options, (resp: http.IncomingMessage) => { + const res = (resp as unknown) as http.IncomingMessage & { + req: http.IncomingMessage; + }; + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + const spans = audit.processSpans(); + assert.strictEqual(spans.length, 1); + assert.ok(spans[0].name.indexOf('GET /') >= 0); + const validations = { + hostname: 'google.fr', + 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 */ + }; + assert.ok(data); + assert.ok( + validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] + ); + assert.ok( + validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] + ); + done(); + }); + }); + }); + } + }); +}); diff --git a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts b/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts deleted file mode 100644 index 25a203c0b4..0000000000 --- a/packages/opentelemetry-plugin-http/test/utils/ProxyTracer.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright 2019, 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 { SpanAuditProcessor } from './SpanAuditProcessor'; -import { - Span, - Tracer, - SpanOptions, - BinaryFormat, - HttpTextFormat, -} from '@opentelemetry/types'; - -// TODO: remove these once we have exporter feature -// https://github.com/open-telemetry/opentelemetry-js/pull/149 -export class ProxyTracer implements Tracer { - private readonly _tracer: Tracer; - constructor(tracer: Tracer, public audit: SpanAuditProcessor) { - this._tracer = tracer; - } - getCurrentSpan(): Span | null { - return this._tracer.getCurrentSpan(); - } - startSpan(name: string, options?: SpanOptions | undefined): Span { - const span = this._tracer.startSpan(name, options); - this.audit.push(span); - return span; - } - withSpan ReturnType>( - span: Span, - fn: T - ): ReturnType { - return this._tracer.withSpan(span, fn); - } - recordSpanData(span: Span): void {} - getBinaryFormat(): BinaryFormat { - throw new Error('Method not implemented.'); - } - getHttpTextFormat(): HttpTextFormat { - return this._tracer.getHttpTextFormat(); - } - bind(target: T, span?: Span | undefined): T { - return this._tracer.bind(target); - } -} diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts deleted file mode 100644 index 849421b573..0000000000 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAudit.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright 2019, 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, - Status, - Attributes, - SpanContext, - Link, - Event, -} from '@opentelemetry/types'; - -export interface SpanAudit { - spanContext: SpanContext; - attributes: Attributes; - name: string; - ended: boolean; - events: Event[]; - links: Link[]; - parentSpanId?: string; - kind: SpanKind; - status: Status; - startTime: number; - endTime: number; -} diff --git a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts index a07ab892d2..d7f56f7bb4 100644 --- a/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts +++ b/packages/opentelemetry-plugin-http/test/utils/SpanAuditProcessor.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import { Span } from '@opentelemetry/types'; -import { SpanAudit } from './SpanAudit'; - +import { Span, ReadableSpan } from '@opentelemetry/basic-tracer'; export class SpanAuditProcessor { - private static skipFields = ['_tracer']; private _spans: Span[]; constructor() { this._spans = []; @@ -28,26 +25,8 @@ export class SpanAuditProcessor { this._spans.push(span); } - processSpans(): SpanAudit[] { - return this._spans.map(span => { - const auditSpan = {} as SpanAudit; - // TODO: use getter or SpanData once available - for (const key in span) { - if ( - span.hasOwnProperty(key) && - !SpanAuditProcessor.skipFields.includes(key) - ) { - /* tslint:disable:no-any */ - (auditSpan as any)[key.replace('_', '')] = - typeof (span as any)[key] === 'object' && - !Array.isArray((span as any)[key]) - ? { ...(span as any)[key] } - : (span as any)[key]; - /* tslint:enable:no-any */ - } - } - return auditSpan; - }); + processSpans(): ReadableSpan[] { + return this._spans.map(span => span.toReadableSpan()); } reset() { diff --git a/packages/opentelemetry-plugin-http/test/utils/TracerTest.ts b/packages/opentelemetry-plugin-http/test/utils/TracerTest.ts new file mode 100644 index 0000000000..6bc4ef865a --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/TracerTest.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2019, 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 { SpanOptions } from '@opentelemetry/types'; +import { NodeTracer } from '@opentelemetry/node-tracer'; +import { BasicTracerConfig, Span } from '@opentelemetry/basic-tracer'; +import { SpanAuditProcessor } from './SpanAuditProcessor'; + +// TODO: remove these once we have exporter feature +// https://github.com/open-telemetry/opentelemetry-js/pull/149 +export class TracerTest extends NodeTracer { + constructor(config: BasicTracerConfig, public audit: SpanAuditProcessor) { + super(config); + } + + startSpan(name: string, options?: SpanOptions): Span { + const span = super.startSpan(name, options) as Span; + this.audit.push(span); + return span; + } +} diff --git a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts index 4f9dbb2204..68cb72fab7 100644 --- a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts @@ -21,10 +21,10 @@ import { AttributeNames } from '../../src/enums/attributeNames'; import { HttpPlugin } from '../../src/http'; import { Utils } from '../../src/utils'; import { DummyPropagation } from './DummyPropagation'; -import { SpanAudit } from './SpanAudit'; +import { ReadableSpan } from '@opentelemetry/basic-tracer'; export const assertSpan = ( - span: SpanAudit, + span: ReadableSpan, kind: SpanKind, validations: { httpStatusCode: number; @@ -67,7 +67,7 @@ export const assertSpan = ( span.attributes[AttributeNames.HTTP_STATUS_CODE], validations.httpStatusCode ); - assert.strictEqual(span.ended, true); + assert.ok(span.endTime); assert.strictEqual(span.links.length, 0); assert.strictEqual(span.events.length, 0); assert.deepStrictEqual( @@ -90,5 +90,8 @@ export const assertSpan = ( if (span.kind === SpanKind.SERVER) { assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY); + } else if (validations.reqHeaders) { + assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); + assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); } }; From f3c1805619dcebf1228bd95f142ec57298aaf733 Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Wed, 28 Aug 2019 13:51:01 -0400 Subject: [PATCH 16/18] feat: export class/enums for https module Signed-off-by: Olivier Albertini --- packages/opentelemetry-plugin-http/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/opentelemetry-plugin-http/src/index.ts diff --git a/packages/opentelemetry-plugin-http/src/index.ts b/packages/opentelemetry-plugin-http/src/index.ts new file mode 100644 index 0000000000..eb4b45d027 --- /dev/null +++ b/packages/opentelemetry-plugin-http/src/index.ts @@ -0,0 +1,5 @@ +export * from './http'; +export * from './types'; +export * from './utils'; +export * from './enums/attributeNames'; +export * from './enums/format'; \ No newline at end of file From d61264213f671ea00a7ae5ced8154a624fbaa66b Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Wed, 28 Aug 2019 16:52:25 -0400 Subject: [PATCH 17/18] refactor: plugin.enable has logger param Signed-off-by: Olivier Albertini --- packages/opentelemetry-plugin-http/src/http.ts | 16 ++-------------- packages/opentelemetry-plugin-http/src/index.ts | 2 +- .../test/functionals/http-disable.test.ts | 2 +- .../test/functionals/http-enable.test.ts | 2 +- .../test/functionals/http-package.test.ts | 2 +- .../test/integrations/http-enable.test.ts | 2 +- 6 files changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index e5c9a65c09..7d8d869c9e 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -14,15 +14,8 @@ * limitations under the License. */ -import { BasePlugin, NoopLogger, isValid } from '@opentelemetry/core'; -import { - Span, - SpanKind, - SpanOptions, - Logger, - Tracer, - Attributes, -} from '@opentelemetry/types'; +import { BasePlugin, isValid } from '@opentelemetry/core'; +import { Span, SpanKind, SpanOptions, Attributes } from '@opentelemetry/types'; import { ClientRequest, IncomingMessage, @@ -51,14 +44,9 @@ import { Utils } from './utils'; export class HttpPlugin extends BasePlugin { static readonly component = 'http'; options!: HttpPluginConfig; - protected _logger!: Logger; - protected readonly _tracer!: Tracer; constructor(readonly moduleName: string, readonly version: string) { super(); - // TODO: remove this once a logger will be passed - // https://github.com/open-telemetry/opentelemetry-js/issues/193 - this._logger = new NoopLogger(); // TODO: remove this once options will be passed // see https://github.com/open-telemetry/opentelemetry-js/issues/210 this.options = {}; diff --git a/packages/opentelemetry-plugin-http/src/index.ts b/packages/opentelemetry-plugin-http/src/index.ts index eb4b45d027..c05341f45b 100644 --- a/packages/opentelemetry-plugin-http/src/index.ts +++ b/packages/opentelemetry-plugin-http/src/index.ts @@ -2,4 +2,4 @@ export * from './http'; export * from './types'; export * from './utils'; export * from './enums/attributeNames'; -export * from './enums/format'; \ No newline at end of file +export * from './enums/format'; diff --git a/packages/opentelemetry-plugin-http/test/functionals/http-disable.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-disable.test.ts index 55ed7a8d0e..d4c5e52a91 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-disable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-disable.test.ts @@ -44,7 +44,7 @@ describe('HttpPlugin', () => { nock.cleanAll(); nock.enableNetConnect(); - plugin.enable(http, tracer); + plugin.enable(http, tracer, tracer.logger); server = http.createServer((request, response) => { response.end('Test Server Response'); }); diff --git a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts index 8037ded424..9d14e7df61 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts @@ -83,7 +83,7 @@ describe('HttpPlugin', () => { }); before(() => { - plugin.enable(http, tracer); + plugin.enable(http, tracer, tracer.logger); const ignoreConfig = [ `http://${hostname}:${serverPort}/ignored/string`, /\/ignored\/regexp$/i, diff --git a/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts index d8e4d16810..6fc8a726f6 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts @@ -57,7 +57,7 @@ describe('Packages', () => { }); before(() => { - plugin.enable(http, tracer); + plugin.enable(http, tracer, tracer.logger); }); after(() => { diff --git a/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts index b5280bdc8d..f3603f6508 100644 --- a/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts @@ -54,7 +54,7 @@ describe('HttpPlugin Integration tests', () => { before(() => { plugin.disable(); - plugin.enable(http, tracer); + plugin.enable(http, tracer, tracer.logger); const ignoreConfig = [ `http://${hostname}:${serverPort}/ignored/string`, /\/ignored\/regexp$/i, From 5b5dcedc3599859f3ddeca9f9890599d4e86b7cc Mon Sep 17 00:00:00 2001 From: Olivier Albertini Date: Fri, 30 Aug 2019 15:24:33 -0400 Subject: [PATCH 18/18] fix: add license header, rename enum files and refactoring refactor: make integration tests mandatory for ci only remove duplicate tests remove dead comments Signed-off-by: Olivier Albertini --- .../{attributeNames.ts => AttributeNames.ts} | 2 - .../src/enums/{format.ts => Format.ts} | 1 - .../opentelemetry-plugin-http/src/http.ts | 4 +- .../opentelemetry-plugin-http/src/index.ts | 20 ++- .../opentelemetry-plugin-http/src/utils.ts | 3 +- .../test/functionals/http-enable.test.ts | 123 ------------------ .../test/integrations/http-enable.test.ts | 25 +++- .../test/utils/DummyPropagation.ts | 1 - .../test/utils/Utils.ts | 29 +++++ .../test/utils/assertSpan.ts | 2 +- 10 files changed, 75 insertions(+), 135 deletions(-) rename packages/opentelemetry-plugin-http/src/enums/{attributeNames.ts => AttributeNames.ts} (96%) rename packages/opentelemetry-plugin-http/src/enums/{format.ts => Format.ts} (89%) create mode 100644 packages/opentelemetry-plugin-http/test/utils/Utils.ts diff --git a/packages/opentelemetry-plugin-http/src/enums/attributeNames.ts b/packages/opentelemetry-plugin-http/src/enums/AttributeNames.ts similarity index 96% rename from packages/opentelemetry-plugin-http/src/enums/attributeNames.ts rename to packages/opentelemetry-plugin-http/src/enums/AttributeNames.ts index 8863dc9b0a..277dbf1776 100644 --- a/packages/opentelemetry-plugin-http/src/enums/attributeNames.ts +++ b/packages/opentelemetry-plugin-http/src/enums/AttributeNames.ts @@ -20,8 +20,6 @@ */ export enum AttributeNames { HTTP_HOSTNAME = 'http.hostname', - // NOT ON OFFICIAL SPEC - ERROR = 'error', COMPONENT = 'component', HTTP_METHOD = 'http.method', HTTP_PATH = 'http.path', diff --git a/packages/opentelemetry-plugin-http/src/enums/format.ts b/packages/opentelemetry-plugin-http/src/enums/Format.ts similarity index 89% rename from packages/opentelemetry-plugin-http/src/enums/format.ts rename to packages/opentelemetry-plugin-http/src/enums/Format.ts index 8f5e3d5a2e..66826b5da6 100644 --- a/packages/opentelemetry-plugin-http/src/enums/format.ts +++ b/packages/opentelemetry-plugin-http/src/enums/Format.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -// Should we export this to the types package in order to expose all values ? export enum Format { HTTP = 'HttpTraceContext', } diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 7d8d869c9e..2a828e2692 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -34,8 +34,8 @@ import { ParsedRequestOptions, HttpRequestArgs, } from './types'; -import { Format } from './enums/format'; -import { AttributeNames } from './enums/attributeNames'; +import { Format } from './enums/Format'; +import { AttributeNames } from './enums/AttributeNames'; import { Utils } from './utils'; /** diff --git a/packages/opentelemetry-plugin-http/src/index.ts b/packages/opentelemetry-plugin-http/src/index.ts index c05341f45b..45181e584f 100644 --- a/packages/opentelemetry-plugin-http/src/index.ts +++ b/packages/opentelemetry-plugin-http/src/index.ts @@ -1,5 +1,21 @@ +/** + * Copyright 2019, 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. + */ + export * from './http'; export * from './types'; export * from './utils'; -export * from './enums/attributeNames'; -export * from './enums/format'; +export * from './enums/AttributeNames'; +export * from './enums/Format'; diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 9103495b5f..b48e186d71 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -22,7 +22,7 @@ import { IncomingHttpHeaders, } from 'http'; import { IgnoreMatcher } from './types'; -import { AttributeNames } from './enums/attributeNames'; +import { AttributeNames } from './enums/AttributeNames'; import * as url from 'url'; /** @@ -140,7 +140,6 @@ export class Utils { static setSpanOnError(span: Span, obj: IncomingMessage | ClientRequest) { obj.on('error', error => { span.setAttributes({ - [AttributeNames.ERROR]: true, [AttributeNames.HTTP_ERROR_NAME]: error.name, [AttributeNames.HTTP_ERROR_MESSAGE]: error.message, }); diff --git a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts index 9d14e7df61..75d14b3a19 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts @@ -26,7 +26,6 @@ import { DummyPropagation } from '../utils/DummyPropagation'; import { httpRequest } from '../utils/httpRequest'; import { TracerTest } from '../utils/TracerTest'; import { SpanAuditProcessor } from '../utils/SpanAuditProcessor'; -import * as url from 'url'; let server: http.Server; const serverPort = 12345; @@ -279,127 +278,5 @@ describe('HttpPlugin', () => { assert.strictEqual(spans.length, 0); }); } - - it('should create a rootSpan for GET requests and add propagation headers', async () => { - nock.enableNetConnect(); - const spans = audit.processSpans(); - assert.strictEqual(spans.length, 0); - await httpRequest.get(`http://google.fr/?query=test`).then(result => { - const spans = audit.processSpans(); - assert.strictEqual(spans.length, 1); - assert.ok(spans[0].name.indexOf('GET /') >= 0); - - const span = spans[0]; - const validations = { - hostname: 'google.fr', - httpStatusCode: result.statusCode!, - httpMethod: 'GET', - pathname: '/', - path: '/?query=test', - resHeaders: result.resHeaders, - reqHeaders: result.reqHeaders, - }; - assertSpan(span, SpanKind.CLIENT, validations); - }); - nock.disableNetConnect(); - }); - - it('custom attributes should show up on client spans', async () => { - nock.enableNetConnect(); - - await httpRequest.get(`http://google.fr/`).then(result => { - const spans = audit.processSpans(); - assert.strictEqual(spans.length, 1); - assert.ok(spans[0].name.indexOf('GET /') >= 0); - - const span = spans[0]; - const validations = { - hostname: 'google.fr', - httpStatusCode: result.statusCode!, - httpMethod: 'GET', - pathname: '/', - resHeaders: result.resHeaders, - reqHeaders: result.reqHeaders, - }; - assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); - assertSpan(span, SpanKind.CLIENT, validations); - }); - nock.disableNetConnect(); - }); - - it('should create a span for GET requests and add propagation headers with Expect headers', async () => { - nock.enableNetConnect(); - const spans = audit.processSpans(); - assert.strictEqual(spans.length, 0); - const options = Object.assign( - { headers: { Expect: '100-continue' } }, - url.parse('http://google.fr/') - ); - await httpRequest.get(options).then(result => { - const spans = audit.processSpans(); - assert.strictEqual(spans.length, 1); - assert.ok(spans[0].name.indexOf('GET /') >= 0); - - const span = spans[0]; - const validations = { - hostname: 'google.fr', - httpStatusCode: 301, - httpMethod: 'GET', - pathname: '/', - resHeaders: result.resHeaders, - reqHeaders: result.reqHeaders, - }; - assertSpan(span, SpanKind.CLIENT, validations); - }); - nock.disableNetConnect(); - }); - 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: http.get(url, options, callback) and following headers: ${JSON.stringify( - headers - )}`, done => { - nock.enableNetConnect(); - const spans = audit.processSpans(); - assert.strictEqual(spans.length, 0); - const options = { headers }; - http.get('http://google.fr/', options, (resp: http.IncomingMessage) => { - const res = (resp as unknown) as http.IncomingMessage & { - req: http.IncomingMessage; - }; - let data = ''; - resp.on('data', chunk => { - data += chunk; - }); - resp.on('end', () => { - const spans = audit.processSpans(); - assert.strictEqual(spans.length, 1); - assert.ok(spans[0].name.indexOf('GET /') >= 0); - const validations = { - hostname: 'google.fr', - 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 */ - }; - assert.ok(data); - assert.ok( - validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] - ); - assert.ok( - validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] - ); - done(); - }); - }); - nock.disableNetConnect(); - }); - } }); }); diff --git a/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts index f3603f6508..5a5f3ec117 100644 --- a/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts @@ -26,6 +26,7 @@ import { httpRequest } from '../utils/httpRequest'; import { TracerTest } from '../utils/TracerTest'; import { SpanAuditProcessor } from '../utils/SpanAuditProcessor'; import * as url from 'url'; +import { Utils } from '../utils/Utils'; const serverPort = 12345; const hostname = 'localhost'; @@ -37,6 +38,22 @@ export const customAttributeFunction = (span: Span): void => { describe('HttpPlugin Integration tests', () => { describe('enable()', () => { + before(function(done) { + // mandatory + if (process.env.CI) { + done(); + return; + } + + Utils.checkInternet(isConnected => { + if (!isConnected) { + this.skip(); + // don't disturbe people + } + done(); + }); + }); + const scopeManager = new AsyncHooksScopeManager(); const httpTextFormat = new DummyPropagation(); const logger = new NoopLogger(); @@ -134,7 +151,13 @@ describe('HttpPlugin Integration tests', () => { resHeaders: result.resHeaders, reqHeaders: result.reqHeaders, }; - assertSpan(span, SpanKind.CLIENT, validations); + try { + assertSpan(span, SpanKind.CLIENT, validations); + } catch (error) { + // temporary redirect is also correct + validations.httpStatusCode = 307; + assertSpan(span, SpanKind.CLIENT, validations); + } }); }); for (const headers of [ diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index f9ce219fb4..4d931e01e3 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { SpanContext, HttpTextFormat } from '@opentelemetry/types'; -// import * as http from 'http'; export class DummyPropagation implements HttpTextFormat { static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; diff --git a/packages/opentelemetry-plugin-http/test/utils/Utils.ts b/packages/opentelemetry-plugin-http/test/utils/Utils.ts new file mode 100644 index 0000000000..5544856604 --- /dev/null +++ b/packages/opentelemetry-plugin-http/test/utils/Utils.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2019, 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 * as dns from 'dns'; + +export class Utils { + static checkInternet(cb: (isConnected: boolean) => void) { + dns.lookup('google.com', err => { + if (err && err.code === 'ENOTFOUND') { + cb(false); + } else { + cb(true); + } + }); + } +} diff --git a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts index 68cb72fab7..f0d832f3f1 100644 --- a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts @@ -17,7 +17,7 @@ import { SpanKind } from '@opentelemetry/types'; import * as assert from 'assert'; import * as http from 'http'; -import { AttributeNames } from '../../src/enums/attributeNames'; +import { AttributeNames } from '../../src/enums/AttributeNames'; import { HttpPlugin } from '../../src/http'; import { Utils } from '../../src/utils'; import { DummyPropagation } from './DummyPropagation';