/
HttpDependencyParser.ts
153 lines (133 loc) · 6.11 KB
/
HttpDependencyParser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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");
import RequestResponseHeaders = require("../Library/RequestResponseHeaders");
import RequestParser = require("./RequestParser");
import CorrelationIdManager = require("../Library/CorrelationIdManager");
/**
* Helper class to read data from the request/response objects and convert them into the telemetry contract
*/
class HttpDependencyParser extends RequestParser {
private correlationId: string;
constructor(requestOptions: string | http.RequestOptions | https.RequestOptions, request: http.ClientRequest) {
super();
if (request && (<any>request).method && requestOptions) {
// The ClientRequest.method property isn't documented, but is always there.
this.method = (<any>request).method;
this.url = HttpDependencyParser._getUrlFromRequestOptions(requestOptions, request);
this.startTime = +new Date();
}
}
/**
* Called when the ClientRequest emits an error event.
*/
public onError(error: Error) {
this._setStatus(undefined, error);
}
/**
* Called when the ClientRequest emits a response event.
*/
public onResponse(response: http.ClientResponse) {
this._setStatus(response.statusCode, undefined);
this.correlationId = Util.getCorrelationContextTarget(response, RequestResponseHeaders.requestContextTargetKey);
}
/**
* Gets a dependency data contract object for a completed ClientRequest.
*/
public getDependencyTelemetry(baseTelemetry?: Contracts.Telemetry, dependencyId?: string): Contracts.DependencyTelemetry {
let urlObject = url.parse(this.url);
urlObject.search = undefined;
urlObject.hash = undefined;
let dependencyName = this.method.toUpperCase() + " " + urlObject.pathname;
let remoteDependencyType = Contracts.RemoteDependencyDataConstants.TYPE_HTTP;
let remoteDependencyTarget = urlObject.hostname;
if (this.correlationId) {
remoteDependencyType = Contracts.RemoteDependencyDataConstants.TYPE_AI;
if (this.correlationId !== CorrelationIdManager.correlationIdPrefix) {
remoteDependencyTarget = urlObject.hostname + " | " + this.correlationId;
}
} else {
remoteDependencyType = Contracts.RemoteDependencyDataConstants.TYPE_HTTP;
}
if (urlObject.port) {
remoteDependencyTarget += ":" + urlObject.port;
}
var dependencyTelemetry: Contracts.DependencyTelemetry & Contracts.Identified = {
id: dependencyId,
name: dependencyName,
data: this.url,
duration: this.duration,
success: this._isSuccess(),
resultCode: this.statusCode ? this.statusCode.toString() : null,
properties: this.properties || {},
dependencyTypeName: remoteDependencyType,
target: remoteDependencyTarget
};
// 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;
}
/**
* 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;
}
// Similarly, 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 && (<any>request).agent.protocol) || undefined;
options.hostname = options.hostname || 'localhost';
return url.format(options);
}
}
export = HttpDependencyParser;