Skip to content

Commit

Permalink
Merge pull request #130 from jasongin/httpdependencies
Browse files Browse the repository at this point in the history
HTTP dependency request tracking
  • Loading branch information
Kamil Szostak authored Nov 3, 2016
2 parents 33a7e74 + ecaf8db commit de41abb
Show file tree
Hide file tree
Showing 17 changed files with 755 additions and 296 deletions.
119 changes: 119 additions & 0 deletions AutoCollection/ClientRequestParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
///<reference path="..\Declarations\node\node.d.ts" />

import http = require("http");
import url = require("url");

import ContractsModule = require("../Library/Contracts");
import Client = require("../Library/Client");
import Logging = require("../Library/Logging");
import Util = require("../Library/Util");
import RequestParser = require("./RequestParser");

/**
* Helper class to read data from the requst/response objects and convert them into the telemetry contract
*/
class ClientRequestParser extends RequestParser {
constructor(requestOptions: string | Object, request: http.ClientRequest) {
super();
if (request && requestOptions) {
// The ClientRequest.method property isn't documented, but is always there.
this.method = (<any>request).method;

this.url = ClientRequestParser._getUrlFromRequestOptions(requestOptions, request);
this.startTime = +new Date();
}
}

/**
* Called when the ClientRequest emits an error event.
*/
public onError(error: Error, properties?: { [key: string]: string }) {
this._setStatus(undefined, error, properties);
}

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

/**
* Gets a dependency data contract object for a completed ClientRequest.
*/
public getDependencyData(): ContractsModule.Contracts.Data<ContractsModule.Contracts.RemoteDependencyData> {
let urlObject = url.parse(this.url);
urlObject.search = undefined;
urlObject.hash = undefined;
let dependencyName = this.method.toUpperCase() + " " + url.format(urlObject);

let remoteDependency = new ContractsModule.Contracts.RemoteDependencyData();
remoteDependency.target = urlObject.hostname;
remoteDependency.name = dependencyName;
remoteDependency.commandName = this.url;
remoteDependency.value = this.duration;
remoteDependency.success = this._isSuccess();
remoteDependency.async = true;
remoteDependency.dependencyTypeName =
ContractsModule.Contracts.DependencyKind[ContractsModule.Contracts.DependencyKind.Http];
remoteDependency.dependencyKind = ContractsModule.Contracts.DependencyKind.Http;
remoteDependency.dependencySource = ContractsModule.Contracts.DependencySourceType.Undefined;
remoteDependency.properties = this.properties || {};

let data = new ContractsModule.Contracts.Data<ContractsModule.Contracts.RemoteDependencyData>();
data.baseType = "Microsoft.ApplicationInsights.RemoteDependencyData";
data.baseData = remoteDependency;

return data;
}

/**
* Builds a URL from request options, using the same logic as http.request(). This is
* necessary because a ClientRequest object does not expose a url property.
*/
private static _getUrlFromRequestOptions(options: any, request: http.ClientRequest) {
if (typeof options === 'string') {
options = url.parse(options);
} else {
// Avoid modifying the original options object.
let originalOptions = options;
options = {};
if (originalOptions) {
Object.keys(originalOptions).forEach(key => {
options[key] = originalOptions[key];
});
}
}

// Oddly, url.format ignores path and only uses pathname and search,
// so create them from the path, if path was specified
if (options.path) {
const parsedQuery = url.parse(options.path);
options.pathname = parsedQuery.pathname;
options.search = parsedQuery.search;
}

// Simiarly, url.format ignores hostname and port if host is specified,
// even if host doesn't have the port, but http.request does not work
// this way. It will use the port if one is not specified in host,
// effectively treating host as hostname, but will use the port specified
// in host if it exists.
if (options.host && options.port) {
// Force a protocol so it will parse the host as the host, not path.
// It is discarded and not used, so it doesn't matter if it doesn't match
const parsedHost = url.parse(`http://${options.host}`);
if (!parsedHost.port && options.port) {
options.hostname = options.host;
delete options.host;
}
}

// Mix in default values used by http.request and others
options.protocol = options.protocol || (<any>request).agent.protocol;
options.hostname = options.hostname || 'localhost';

return url.format(options);
}
}

export = ClientRequestParser;
73 changes: 73 additions & 0 deletions AutoCollection/ClientRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
///<reference path="..\Declarations\node\node.d.ts" />

import http = require("http");
import url = require("url");

import ContractsModule = require("../Library/Contracts");
import Client = require("../Library/Client");
import Logging = require("../Library/Logging");
import Util = require("../Library/Util");
import ClientRequestParser = require("./ClientRequestParser");

class AutoCollectClientRequests {

public static INSTANCE: AutoCollectClientRequests;

private _client:Client;
private _isEnabled: boolean;
private _isInitialized: boolean;

constructor(client:Client) {
if (!!AutoCollectClientRequests.INSTANCE) {
throw new Error("Client request tracking should be configured from the applicationInsights object");
}

AutoCollectClientRequests.INSTANCE = this;
this._client = client;
}

public enable(isEnabled:boolean) {
this._isEnabled = isEnabled;
if (this._isEnabled && !this._isInitialized) {
this._initialize();
}
}

public isInitialized() {
return this._isInitialized;
}

private _initialize() {
this._isInitialized = true;
// Note there's no need to redirect https.request because it just uses http.request.
const originalRequest = http.request;
http.request = (options, ...requestArgs) => {
const request: http.ClientRequest = originalRequest.call(
http, options, ...requestArgs);
if (request && options && !options['disableAppInsigntsAutoCollection']) {
AutoCollectClientRequests.trackRequest(this._client, options, request);
}
return request;
};
}

public static trackRequest(client: Client, requestOptions: any, request: http.ClientRequest,
properties?: { [key: string]: string }) {
let requestParser = new ClientRequestParser(requestOptions, request);
request.on('response', (response: http.ClientResponse) => {
requestParser.onResponse(response, properties);
client.track(requestParser.getDependencyData());
});
request.on('error', (e: Error) => {
requestParser.onError(e, properties);
client.track(requestParser.getDependencyData());
});
}

public dispose() {
AutoCollectClientRequests.INSTANCE = null;
this._isInitialized = false;
}
}

export = AutoCollectClientRequests;
4 changes: 2 additions & 2 deletions AutoCollection/Performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class AutoCollectPerformance {

constructor(client: Client) {
if(!!AutoCollectPerformance.INSTANCE) {
throw new Error("Exception tracking should be configured from the applicationInsights object");
throw new Error("Performance tracking should be configured from the applicationInsights object");
}

AutoCollectPerformance.INSTANCE = this;
Expand Down Expand Up @@ -215,7 +215,7 @@ class AutoCollectPerformance {
this._trackLegacyPerformance(PerfCounterType.ProcessorTime, totalUser / combinedTotal);
this._trackLegacyPerformance(PerfCounterType.PercentProcessorTime, (combinedTotal - totalIdle) / combinedTotal);
}

this._lastCpus = cpus;
}

Expand Down
47 changes: 47 additions & 0 deletions AutoCollection/RequestParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

/**
* Base class for helpers that read data from HTTP requst/response objects and convert them
* into the telemetry contract objects.
*/
abstract class RequestParser {
protected method: string;
protected url: string;
protected startTime: number;
protected duration: number;
protected statusCode: number;
protected properties: { [key: string]: string };

protected RequestParser() {
this.startTime = +new Date();
}

protected _setStatus(status: number, error: Error | string, properties: { [key: string]: string }) {
let endTime = +new Date();
this.duration = endTime - this.startTime;
this.statusCode = status;

if (error) {
if(!properties) {
properties = <{[key: string]: string}>{};
}

if (typeof error === "string") {
properties["error"] = error;
} else if (error instanceof Error) {
properties["error"] = error.message;
} else if (typeof error === "object") {
for (var key in error) {
properties[key] = error[key] && error[key].toString && error[key].toString();
}
}
}

this.properties = properties;
}

protected _isSuccess() {
return (0 < this.statusCode) && (this.statusCode < 400);
}
}

export = RequestParser;
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,22 @@ import ContractsModule = require("../Library/Contracts");
import Client = require("../Library/Client");
import Logging = require("../Library/Logging");
import Util = require("../Library/Util");
import RequestParser = require("./RequestParser");

/**
* Helper class to read data from the requst/response objects and convert them into the telemetry contract
*/
class RequestDataHelper {
class ServerRequestParser extends RequestParser {
private static keys = new ContractsModule.Contracts.ContextTagKeys();

private method:string;
private url:string;
private startTime:number;
private rawHeaders:string[];
private socketRemoteAddress:string;
private connectionRemoteAddress:string;
private legacySocketRemoteAddress:string;
private userAgent: string;

private duration:number;
private statusCode:number;
private properties:{[key: string]:string};

constructor(request:http.ServerRequest) {
super();
if (request) {
this.method = request.method;
this.url = this._getAbsoluteUrl(request);
Expand All @@ -42,16 +37,16 @@ class RequestDataHelper {
}
}

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

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

if (ellapsedMilliseconds) {
this.duration = ellapsedMilliseconds;
} else {
var endTime = +new Date();
this.duration = endTime - this.startTime;
}

this.statusCode = response.statusCode;
this.properties = properties;
}

public getRequestData():ContractsModule.Contracts.Data<ContractsModule.Contracts.RequestData> {
Expand All @@ -63,7 +58,7 @@ class RequestDataHelper {
requestData.url = this.url;
requestData.duration = Util.msToTimeSpan(this.duration);
requestData.responseCode = this.statusCode ? this.statusCode.toString() : null;
requestData.success = this._isSuccess(this.statusCode);
requestData.success = this._isSuccess();
requestData.properties = this.properties;

var data = new ContractsModule.Contracts.Data<ContractsModule.Contracts.RequestData>();
Expand All @@ -80,37 +75,33 @@ class RequestDataHelper {
newTags[key] = tags[key];
}

newTags[RequestDataHelper.keys.locationIp] = this._getIp();
newTags[RequestDataHelper.keys.sessionId] = this._getId("ai_session");
newTags[RequestDataHelper.keys.userId] = this._getId("ai_user");
newTags[RequestDataHelper.keys.userAgent] = this.userAgent;
newTags[RequestDataHelper.keys.operationName] = this.method + " " + url.parse(this.url).pathname;
newTags[ServerRequestParser.keys.locationIp] = this._getIp();
newTags[ServerRequestParser.keys.sessionId] = this._getId("ai_session");
newTags[ServerRequestParser.keys.userId] = this._getId("ai_user");
newTags[ServerRequestParser.keys.userAgent] = this.userAgent;
newTags[ServerRequestParser.keys.operationName] = this.method + " " + url.parse(this.url).pathname;

return newTags;
}

private _isSuccess(statusCode:number) {
return statusCode && (statusCode < 400); // todo: this could probably be improved
}

private _getAbsoluteUrl(request:http.ServerRequest):string {
if (!request.headers) {
return request.url;
}

var encrypted = <any>request.connection ? (<any>request.connection).encrypted : null;
var requestUrl = url.parse(request.url);

var pathName = requestUrl.pathname;
var search = requestUrl.search;

var absoluteUrl = url.format({
protocol: encrypted ? "https" : "http",
host: request.headers.host,
pathname: pathName,
search: search
});

return absoluteUrl;
}

Expand Down Expand Up @@ -146,15 +137,15 @@ class RequestDataHelper {
}

private _getId(name: string) {
var cookie = (this.rawHeaders && this.rawHeaders["cookie"] &&
var cookie = (this.rawHeaders && this.rawHeaders["cookie"] &&
typeof this.rawHeaders["cookie"] === 'string' && this.rawHeaders["cookie"]) || "";
var value = RequestDataHelper.parseId(Util.getCookie(name, cookie));
var value = ServerRequestParser.parseId(Util.getCookie(name, cookie));
return value;
}

public static parseId(cookieValue: string): string{
return cookieValue.substr(0, cookieValue.indexOf('|'));
}
}

export = RequestDataHelper;
export = ServerRequestParser;
Loading

0 comments on commit de41abb

Please sign in to comment.