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

HTTP dependency request tracking #129

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 + " " + url.format(urlObject);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make sure that this.method is upper-cased? It should be consistent with the JavaScript SDK - see

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


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;
78 changes: 78 additions & 0 deletions AutoCollection/ClientRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
///<reference path="..\Declarations\node\node.d.ts" />

import http = require("http");
import https = require("https");
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;
this._initializeHttpModule(http);
this._initializeHttpModule(https);
}

private _initializeHttpModule(httpModule: any) {
const originalRequest = httpModule.request;
httpModule.request = (options, ...requestArgs) => {
const request: http.ClientRequest = originalRequest.call(
originalRequest, options, ...requestArgs);
if (request) {
AutoCollectClientRequests.trackRequest(this._client, options, request);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to reuse trackDependency method? Ideally, we want to have only one function to build a RemoteDependencyData object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible, but I think that would make the code less consistent overall.

This dependency request code is following the same pattern as the already existing server request code. That code does not call the Client.trackRequest method either: the helper constructs a RequestData object directly instead.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's keep it as it is.

}
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;
Loading