-
Notifications
You must be signed in to change notification settings - Fork 141
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
|
||
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; |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
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; |
There was a problem hiding this comment.
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 - seeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.