-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #130 from jasongin/httpdependencies
HTTP dependency request tracking
- Loading branch information
Showing
17 changed files
with
755 additions
and
296 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.