Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bugs for 1.0.2 #369

Merged
merged 13 commits into from
Feb 21, 2018
49 changes: 35 additions & 14 deletions AutoCollection/CorrelationContextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,34 @@ export class CorrelationContextManager {
* Enables the CorrelationContextManager.
*/
public static enable() {
if (this.enabled) {
return;
}

if (!this.isNodeVersionCompatible()) {
this.enabled = false;
return;
}

// Load in Zone.js
require("zone.js");


// Run patches for Zone.js
if (!this.hasEverEnabled) {
if (!CorrelationContextManager.hasEverEnabled) {
this.hasEverEnabled = true;

// Load in Zone.js
try {
// Require zone if we can't detect its presence - guarded because of issue #346
// Note that usually multiple requires of zone.js does not error - but we see reports of it happening
// in the Azure Functions environment.
// This indicates that the file is being included multiple times in the same global scope,
// averting require's cache somehow.
if (typeof Zone === "undefined") {
require("zone.js");
}
} catch (e) {
// Zone was already loaded even though we couldn't find its global variable
Logging.warn("Failed to require zone.js");
}

channel.addContextPreservation((cb) => {
return Zone.current.wrap(cb, "AI-ContextPreservation");
})
Expand Down Expand Up @@ -193,7 +209,7 @@ export class CorrelationContextManager {
if ((<any>orig).prepareStackTrace) {
(<any>orig).stackRewrite= false;
var stackTrace = (<any>orig).prepareStackTrace;
(<any>orig).prepareStackTrace = (e: any, s: any) => {
(<any>orig).prepareStackTrace = (e: any, s: any[]) => {
// Remove some AI and Zone methods from the stack trace
// Otherwise we leave side-effects

Expand All @@ -203,20 +219,25 @@ export class CorrelationContextManager {
// Zone | Zone | CorrelationContextManager | CorrelationContextManager | User
var foundOne = false;
for (var i=0; i<s.length; i++) {
if (s[i].getFileName().indexOf("AutoCollection/CorrelationContextManager") === -1 &&
s[i].getFileName().indexOf("AutoCollection\\CorrelationContextManager") === -1) {

if (foundOne) {
break;
let fileName = s[i].getFileName();
if (fileName) {
if (fileName.indexOf("AutoCollection/CorrelationContextManager") === -1 &&
fileName.indexOf("AutoCollection\\CorrelationContextManager") === -1) {

if (foundOne) {
break;
}
} else {
foundOne = true;
}
} else {
foundOne = true;
}
}
// Loop above goes one extra step
i = Math.max(0, i - 1);

s.splice(0, i);
if (foundOne) {
s.splice(0, i);
}
return stackTrace(e, s);
}
}
Expand Down
61 changes: 35 additions & 26 deletions AutoCollection/HttpDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import http = require("http");
import https = require("https");
import url = require("url");

import Contracts = require("../Declarations/Contracts");
import TelemetryClient = require("../Library/TelemetryClient");
import Logging = require("../Library/Logging");
import Util = require("../Library/Util");
Expand Down Expand Up @@ -66,7 +67,7 @@ class AutoCollectHttpDependencies {
(<any>request)[AutoCollectHttpDependencies.alreadyAutoCollectedFlag] = true;

if (request && options && shouldCollect) {
AutoCollectHttpDependencies.trackRequest(this._client, options, request);
AutoCollectHttpDependencies.trackRequest(this._client, {options: options, request: request});
}
};

Expand Down Expand Up @@ -106,14 +107,13 @@ class AutoCollectHttpDependencies {
* Tracks an outgoing request. Because it may set headers this method must be called before
* writing content to or ending the request.
*/
public static trackRequest(client: TelemetryClient, requestOptions: string | http.RequestOptions | https.RequestOptions, request: http.ClientRequest,
properties?: { [key: string]: string }) {
if (!requestOptions || !request || !client) {
Logging.info("AutoCollectHttpDependencies.trackRequest was called with invalid parameters: ", !requestOptions, !request, !client);
public static trackRequest(client: TelemetryClient, telemetry: Contracts.NodeHttpDependencyTelemetry) {
if (!telemetry.options || !telemetry.request || !client) {
Logging.info("AutoCollectHttpDependencies.trackRequest was called with invalid parameters: ", !telemetry.options, !telemetry.request, !client);
return;
}

let requestParser = new HttpDependencyParser(requestOptions, request);
let requestParser = new HttpDependencyParser(telemetry.options, telemetry.request);

const currentContext = CorrelationContextManager.getCurrentContext();
const uniqueRequestId = currentContext && currentContext.operation && (currentContext.operation.parentId + AutoCollectHttpDependencies.requestNumber++ + '.');
Expand All @@ -122,53 +122,62 @@ class AutoCollectHttpDependencies {
// The getHeader/setHeader methods aren't available on very old Node versions, and
// are not included in the v0.10 type declarations currently used. So check if the
// methods exist before invoking them.
if (Util.canIncludeCorrelationHeader(client, requestParser.getUrl()) &&
request['getHeader'] && request['setHeader']) {
if (Util.canIncludeCorrelationHeader(client, requestParser.getUrl()) && telemetry.request.getHeader && telemetry.request.setHeader) {
if (client.config && client.config.correlationId) {
const correlationHeader = request['getHeader'](RequestResponseHeaders.requestContextHeader);
const correlationHeader = telemetry.request.getHeader(RequestResponseHeaders.requestContextHeader);
if (correlationHeader) {
const components = correlationHeader.split(",");
const key = `${RequestResponseHeaders.requestContextSourceKey}=`;
const roleNameKey = `${RequestResponseHeaders.requestContextSourceRoleNameKey}=`;
if (!components.some((value) => value.substring(0,key.length) === key)) {
request['setHeader'](
telemetry.request.setHeader(
RequestResponseHeaders.requestContextHeader,
`${correlationHeader},${RequestResponseHeaders.requestContextSourceKey}=${client.config.correlationId},${RequestResponseHeaders.requestContextSourceRoleNameKey}=${client.context.tags[client.context.keys.cloudRole]}`);
}
} else {
request['setHeader'](
telemetry.request.setHeader(
RequestResponseHeaders.requestContextHeader,
`${RequestResponseHeaders.requestContextSourceKey}=${client.config.correlationId},${RequestResponseHeaders.requestContextSourceRoleNameKey}=${client.context.tags[client.context.keys.cloudRole]}`);
}
}

if (currentContext && currentContext.operation) {
request['setHeader'](RequestResponseHeaders.requestIdHeader, uniqueRequestId);
telemetry.request.setHeader(RequestResponseHeaders.requestIdHeader, uniqueRequestId);
// Also set legacy headers
request['setHeader'](RequestResponseHeaders.parentIdHeader, currentContext.operation.id);
request['setHeader'](RequestResponseHeaders.rootIdHeader, uniqueRequestId);
telemetry.request.setHeader(RequestResponseHeaders.parentIdHeader, currentContext.operation.id);
telemetry.request.setHeader(RequestResponseHeaders.rootIdHeader, uniqueRequestId);

const correlationContextHeader = (<PrivateCustomProperties>currentContext.customProperties).serializeToHeader();
if (correlationContextHeader) {
request['setHeader'](RequestResponseHeaders.correlationContextHeader, correlationContextHeader);
telemetry.request.setHeader(RequestResponseHeaders.correlationContextHeader, correlationContextHeader);
}
}
}

// Collect dependency telemetry about the request when it finishes.
if (request.on) {
request.on('response', (response: http.ClientResponse) => {
requestParser.onResponse(response, properties);
var context : { [name: string]: any; } = { "http.RequestOptions": requestOptions, "http.ClientRequest": request, "http.ClientResponse": response };
var dependencyTelemetry = requestParser.getDependencyTelemetry(uniqueRequestId);
dependencyTelemetry.contextObjects = context;
if (telemetry.request.on) {
telemetry.request.on('response', (response: http.ClientResponse) => {
requestParser.onResponse(response);

var dependencyTelemetry = requestParser.getDependencyTelemetry(telemetry, uniqueRequestId);

dependencyTelemetry.contextObjects = dependencyTelemetry.contextObjects || {};
dependencyTelemetry.contextObjects["http.RequestOptions"] = telemetry.options;
dependencyTelemetry.contextObjects["http.ClientRequest"] = telemetry.request;
dependencyTelemetry.contextObjects["http.ClientResponse"] = response;

client.trackDependency(dependencyTelemetry);
});
request.on('error', (e: Error) => {
requestParser.onError(e, properties);
var context : { [name: string]: any; } = { "http.RequestOptions": requestOptions, "http.ClientRequest": request, "Error": e };
var dependencyTelemetry = requestParser.getDependencyTelemetry(uniqueRequestId);
dependencyTelemetry.contextObjects = context;
telemetry.request.on('error', (e: Error) => {
requestParser.onError(e);

var dependencyTelemetry = requestParser.getDependencyTelemetry(telemetry, uniqueRequestId);

dependencyTelemetry.contextObjects = dependencyTelemetry.contextObjects || {};
dependencyTelemetry.contextObjects["http.RequestOptions"] = telemetry.options;
dependencyTelemetry.contextObjects["http.ClientRequest"] = telemetry.request;
dependencyTelemetry.contextObjects["Error"] = e;

client.trackDependency(dependencyTelemetry);
});
}
Expand Down
32 changes: 25 additions & 7 deletions AutoCollection/HttpDependencyParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,23 @@ class HttpDependencyParser extends RequestParser {
/**
* Called when the ClientRequest emits an error event.
*/
public onError(error: Error, properties?: { [key: string]: string }) {
this._setStatus(undefined, error, properties);
public onError(error: Error) {
this._setStatus(undefined, error);
}

/**
* Called when the ClientRequest emits a response event.
*/
public onResponse(response: http.ClientResponse, properties?: { [key: string]: string }) {
this._setStatus(response.statusCode, undefined, properties);
public onResponse(response: http.ClientResponse) {
this._setStatus(response.statusCode, undefined);
this.correlationId = Util.getCorrelationContextTarget(response, RequestResponseHeaders.requestContextTargetKey);
this.targetRoleName = Util.getCorrelationContextTarget(response, RequestResponseHeaders.requestContextTargetRoleNameKey);
}

/**
* Gets a dependency data contract object for a completed ClientRequest.
*/
public getDependencyTelemetry(dependencyId?: string): Contracts.DependencyTelemetry {
public getDependencyTelemetry(baseTelemetry?: Contracts.Telemetry, dependencyId?: string): Contracts.DependencyTelemetry {
let urlObject = url.parse(this.url);
urlObject.search = undefined;
urlObject.hash = undefined;
Expand All @@ -66,7 +66,7 @@ class HttpDependencyParser extends RequestParser {
remoteDependencyType = Contracts.RemoteDependencyDataConstants.TYPE_HTTP;
}

var telemetry: Contracts.DependencyTelemetry & Contracts.Identified = {
var dependencyTelemetry: Contracts.DependencyTelemetry & Contracts.Identified = {
id: dependencyId,
name: dependencyName,
data: this.url,
Expand All @@ -78,7 +78,25 @@ class HttpDependencyParser extends RequestParser {
target: remoteDependencyTarget
};

return telemetry;
// We should keep any parameters the user passed in
// Except the fields defined above in requestTelemetry, which take priority
// Except the properties field, where they're merged instead, with baseTelemetry taking priority
if (baseTelemetry) {
// Copy missing fields
for (let key in baseTelemetry) {
if (!(<any>dependencyTelemetry)[key]) {
(<any>dependencyTelemetry)[key] = (<any>baseTelemetry)[key];
}
}
// Merge properties
if (baseTelemetry.properties) {
for (let key in baseTelemetry.properties) {
dependencyTelemetry.properties[key] = baseTelemetry.properties[key];
}
}
}

return dependencyTelemetry;
}

/**
Expand Down
34 changes: 29 additions & 5 deletions AutoCollection/HttpRequestParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,25 @@ class HttpRequestParser extends RequestParser {
}
}

public onError(error: Error | string, properties?: { [key: string]: string }, ellapsedMilliseconds?: number) {
this._setStatus(undefined, error, properties);
public onError(error: Error | string, ellapsedMilliseconds?: number) {
this._setStatus(undefined, error);

// This parameter is only for overrides. setStatus handles this internally for the autocollected case
if (ellapsedMilliseconds) {
this.duration = ellapsedMilliseconds;
}
}

public onResponse(response: http.ServerResponse, properties?: { [key: string]: string }, ellapsedMilliseconds?: number) {
this._setStatus(response.statusCode, undefined, properties);
public onResponse(response: http.ServerResponse, ellapsedMilliseconds?: number) {
this._setStatus(response.statusCode, undefined);

// This parameter is only for overrides. setStatus handles this internally for the autocollected case
if (ellapsedMilliseconds) {
this.duration = ellapsedMilliseconds;
}
}

public getRequestTelemetry(): Contracts.RequestTelemetry {
public getRequestTelemetry(baseTelemetry?: Contracts.Telemetry): Contracts.RequestTelemetry {
var requestTelemetry: Contracts.RequestTelemetry & Contracts.Identified = {
id: this.requestId,
name: this.method + " " + url.parse(this.url).pathname,
Expand All @@ -71,6 +77,24 @@ class HttpRequestParser extends RequestParser {
properties: this.properties
};

// We should keep any parameters the user passed in
// Except the fields defined above in requestTelemetry, which take priority
// Except the properties field, where they're merged instead, with baseTelemetry taking priority
if (baseTelemetry) {
// Copy missing fields
for (let key in baseTelemetry) {
if (!(<any>requestTelemetry)[key]) {
(<any>requestTelemetry)[key] = (<any>baseTelemetry)[key];
}
}
// Merge properties
if (baseTelemetry.properties) {
for (let key in baseTelemetry.properties) {
requestTelemetry.properties[key] = baseTelemetry.properties[key];
}
}
}

return requestTelemetry;
}

Expand Down