Skip to content

Commit a5feb12

Browse files
committed
lib: add tracing support for http2
PR-URL: #266 Reviewed-By: Juan José Arboleda <[email protected]> Reviewed-By: Rafael Gonzaga <[email protected]>
1 parent d3adef7 commit a5feb12

12 files changed

+1031
-6
lines changed

lib/internal/nsolid_diag.js

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const nsolidApi = internalBinding('nsolid_api');
44
const {
55
nsolid_counts,
6+
nsolid_span_id_s,
67
nsolid_tracer_s,
78
nsolid_consts,
89
} = nsolidApi;
@@ -14,6 +15,7 @@ const {
1415
} = require('internal/otel/core');
1516

1617
const {
18+
extractSpanContextFromHttpHeaders,
1719
generateSpan,
1820
nsolidTracer,
1921
} = require('internal/nsolid_trace');
@@ -28,21 +30,34 @@ const {
2830
kSpanHttpClient,
2931
kSpanHttpMethod,
3032
kSpanHttpReqUrl,
33+
kSpanHttpServer,
3134
kSpanHttpStatusCode,
3235
} = nsolid_consts;
3336

3437
const undiciFetch = dc.tracingChannel('undici:fetch');
38+
const http2Client = dc.tracingChannel('http2.client');
39+
const http2Server = dc.tracingChannel('http2.server');
3540

3641
// To lazy load the http2 constants
3742
let http2Constants;
3843

3944
let tracingEnabled = false;
4045

41-
const fetchSubscribeListener = (message, name) => {};
46+
const subscribeListener = (message, name) => {};
4247

4348
function disableTracing() {
4449
undiciFetch.start.unbindStore();
45-
dc.unsubscribe('tracing:undici:fetch:start', fetchSubscribeListener);
50+
dc.unsubscribe('tracing:undici:fetch:start', subscribeListener);
51+
http2Client.start.unbindStore();
52+
dc.unsubscribe('tracing:http2.client:start', subscribeListener);
53+
http2Server.start.unbindStore();
54+
dc.unsubscribe('tracing:http2.server:start', subscribeListener);
55+
}
56+
57+
function extractHttp2Url(headers, http2Constants) {
58+
const { HTTP2_HEADER_SCHEME, HTTP2_HEADER_PATH } = http2Constants;
59+
const authority = require('internal/http2/util').getAuthority(headers);
60+
return `${headers[HTTP2_HEADER_SCHEME]}://${authority}${headers[HTTP2_HEADER_PATH]}`;
4661
}
4762

4863
function enableTracing() {
@@ -66,7 +81,52 @@ function enableTracing() {
6681
return api.trace.setSpan(api.context.active(), span);
6782
});
6883

69-
dc.subscribe('tracing:undici:fetch:start', fetchSubscribeListener);
84+
dc.subscribe('tracing:http2.client:start', subscribeListener);
85+
86+
http2Client.start.bindStore(contextManager._getALS(), (data) => {
87+
const api = getApi();
88+
const tracer = api.trace.getTracer('http2');
89+
http2Constants ||= require('internal/http2/core').constants;
90+
const { headers } = data;
91+
const method = headers[http2Constants.HTTP2_HEADER_METHOD];
92+
const url = extractHttp2Url(headers, http2Constants);
93+
const span = tracer.startSpan(`HTTP ${method}`,
94+
{ internal: true,
95+
kind: api.SpanKind.CLIENT,
96+
type: kSpanHttpClient });
97+
span._pushSpanDataString(kSpanHttpMethod, method);
98+
span._pushSpanDataString(kSpanHttpReqUrl, url);
99+
const { spanId, traceId } = span.spanContext();
100+
if (span._isSampled()) {
101+
headers.traceparent = `00-${traceId}-${spanId}-01`;
102+
} else {
103+
headers.traceparent = `00-${traceId}-${spanId}-00`;
104+
}
105+
106+
return api.trace.setSpan(api.context.active(), span);
107+
});
108+
109+
dc.subscribe('tracing:http2.client:start', subscribeListener);
110+
111+
http2Server.start.bindStore(contextManager._getALS(), (data) => {
112+
const api = getApi();
113+
const tracer = api.trace.getTracer('http2');
114+
http2Constants ||= require('internal/http2/core').constants;
115+
const { headers } = data;
116+
const method = headers[http2Constants.HTTP2_HEADER_METHOD];
117+
const url = extractHttp2Url(headers, http2Constants);
118+
const ctxt = extractSpanContextFromHttpHeaders(api.ROOT_CONTEXT, headers);
119+
const span = tracer.startSpan(`HTTP ${method}`,
120+
{ internal: true,
121+
kind: api.SpanKind.SERVER,
122+
type: kSpanHttpServer },
123+
ctxt);
124+
span._pushSpanDataString(kSpanHttpMethod, method);
125+
span._pushSpanDataString(kSpanHttpReqUrl, url);
126+
return api.trace.setSpan(api.context.active(), span);
127+
});
128+
129+
dc.subscribe('tracing:http2.server:start', subscribeListener);
70130
}
71131

72132
nsolidTracer.on('flagsUpdated', () => {
@@ -130,41 +190,119 @@ dc.subscribe('http2.client.stream.created', ({ stream }) => {
130190
start: now(),
131191
response: false,
132192
};
193+
194+
if (generateSpan(kSpanHttpClient)) {
195+
const api = getApi();
196+
const span = api.trace.getSpan(api.context.active());
197+
if (span) {
198+
stream[nsolid_span_id_s] = span;
199+
}
200+
}
133201
});
134202

135-
dc.subscribe('http2.client.stream.finish', ({ stream, flags }) => {
203+
dc.subscribe('http2.client.stream.finish', ({ stream, headers }) => {
136204
stream[nsolid_tracer_s].response = true;
205+
http2Constants ||= require('internal/http2/core').constants;
206+
if (generateSpan(kSpanHttpClient)) {
207+
const span = stream[nsolid_span_id_s];
208+
if (span) {
209+
const status = headers[http2Constants.HTTP2_HEADER_STATUS];
210+
span._pushSpanDataUint64(kSpanHttpStatusCode, status);
211+
if (status >= 400) {
212+
span.setStatus({ code: getApi().SpanStatusCode.ERROR });
213+
}
214+
}
215+
}
216+
});
217+
218+
dc.subscribe('http2.client.stream.error', ({ stream, error }) => {
219+
if (generateSpan(kSpanHttpClient)) {
220+
const span = stream[nsolid_span_id_s];
221+
if (span) {
222+
span.recordException(error);
223+
span.setStatus({
224+
code: getApi().SpanStatusCode.ERROR,
225+
message: error.message,
226+
});
227+
span.end();
228+
}
229+
}
137230
});
138231

139232
dc.subscribe('http2.client.stream.close', ({ stream, code }) => {
140233
http2Constants ||= require('internal/http2/core').constants;
141234
const tracingInfo = stream[nsolid_tracer_s];
235+
const span = stream[nsolid_span_id_s];
142236
if (code === http2Constants.NGHTTP2_NO_ERROR && tracingInfo.response) {
143237
nsolid_counts[kHttpClientCount]++;
144238
nsolidApi.pushClientBucket(now() - tracingInfo.start);
145239
} else {
146240
nsolid_counts[kHttpClientAbortCount]++;
241+
if (span) {
242+
span.setStatus({
243+
code: getApi().SpanStatusCode.ERROR,
244+
});
245+
}
147246
}
247+
248+
span?.end();
148249
});
149250

150-
dc.subscribe('http2.server.stream.start', ({ stream }) => {
251+
dc.subscribe('http2.server.stream.start', ({ stream, headers }) => {
151252
stream[nsolid_tracer_s] = {
152253
start: now(),
153254
response: false,
154255
};
256+
257+
if (generateSpan(kSpanHttpServer)) {
258+
const api = getApi();
259+
const span = api.trace.getSpan(api.context.active());
260+
if (span) {
261+
stream[nsolid_span_id_s] = span;
262+
}
263+
}
155264
});
156265

157-
dc.subscribe('http2.server.stream.finish', ({ stream, flags }) => {
266+
dc.subscribe('http2.server.stream.finish', ({ stream, headers }) => {
158267
stream[nsolid_tracer_s].response = true;
159268
});
160269

270+
dc.subscribe('http2.server.stream.error', ({ stream, error }) => {
271+
if (generateSpan(kSpanHttpServer)) {
272+
const span = stream[nsolid_span_id_s];
273+
if (span) {
274+
span.recordException(error);
275+
span.setStatus({
276+
code: getApi().SpanStatusCode.ERROR,
277+
message: error.message,
278+
});
279+
span.end();
280+
}
281+
}
282+
});
283+
161284
dc.subscribe('http2.server.stream.close', ({ stream, code }) => {
162285
http2Constants ||= require('internal/http2/core').constants;
163286
const tracingInfo = stream[nsolid_tracer_s];
287+
const span = stream[nsolid_span_id_s];
164288
if (code === http2Constants.NGHTTP2_NO_ERROR && tracingInfo.response) {
165289
nsolid_counts[kHttpServerCount]++;
166290
nsolidApi.pushServerBucket(now() - tracingInfo.start);
167291
} else {
168292
nsolid_counts[kHttpServerAbortCount]++;
293+
if (span) {
294+
span.setStatus({
295+
code: getApi().SpanStatusCode.ERROR,
296+
});
297+
}
298+
}
299+
300+
if (span) {
301+
if (stream.headersSent) {
302+
const status = stream.sentHeaders[http2Constants.HTTP2_HEADER_STATUS];
303+
span._pushSpanDataUint64(kSpanHttpStatusCode, status);
304+
}
305+
306+
span.end();
169307
}
170308
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Flags: --dns-result-order=ipv4first
2+
'use strict';
3+
const common = require('../../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const { checkTracesOnExit } = require('../../common/nsolid-traces');
7+
const { setupNSolid } = require('./utils');
8+
const http2 = require('http2');
9+
const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
10+
const binding = require(bindingPath);
11+
12+
function setupTracesCheck(port, addresses) {
13+
const expectedTraces = [
14+
{
15+
attributes: {
16+
'http.method': 'GET',
17+
'http.url': `http://localhost:${port}/`,
18+
},
19+
end_reason: binding.kSpanEndOk,
20+
name: 'HTTP GET',
21+
parentId: '0000000000000000',
22+
thread_id: 0,
23+
kind: binding.kClient,
24+
type: binding.kSpanHttpClient,
25+
events: [
26+
{
27+
attributes: {
28+
'exception.message': 'The operation was aborted',
29+
'exception.type': 'ABORT_ERR',
30+
},
31+
name: 'exception',
32+
},
33+
],
34+
status: {
35+
code: 2, // ERROR
36+
message: 'The operation was aborted',
37+
},
38+
},
39+
{
40+
attributes: {
41+
'dns.address': [{ address: addresses[0].address,
42+
family: addresses[0].family }],
43+
'dns.hostname': 'localhost',
44+
'dns.op_type': binding.kDnsLookup,
45+
},
46+
end_reason: binding.kSpanEndOk,
47+
name: 'DNS lookup',
48+
thread_id: 0,
49+
kind: binding.kClient,
50+
type: binding.kSpanDns,
51+
},
52+
];
53+
54+
checkTracesOnExit(binding, expectedTraces);
55+
}
56+
57+
setupNSolid(common.mustSucceed(({ addresses }) => {
58+
const server = http2.createServer();
59+
const controller = new AbortController();
60+
61+
server.on('stream', common.mustNotCall());
62+
63+
server.listen(0, common.mustCall(() => {
64+
const port = server.address().port;
65+
setupTracesCheck(port, addresses);
66+
const client = http2.connect(`http://localhost:${port}`);
67+
client.on('close', common.mustCall());
68+
69+
const { signal } = controller;
70+
controller.abort();
71+
72+
const req = client.request({}, { signal });
73+
74+
req.on('error', common.mustCall());
75+
req.on('close', common.mustCall(() => {
76+
client.close(common.mustCall(() => {
77+
server.close();
78+
}));
79+
}));
80+
}));
81+
}));
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Flags: --dns-result-order=ipv4first
2+
'use strict';
3+
const common = require('../../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const { checkTracesOnExit } = require('../../common/nsolid-traces');
7+
const { setupNSolid } = require('./utils');
8+
const http2 = require('http2');
9+
const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
10+
const binding = require(bindingPath);
11+
12+
function setupTracesCheck(port, addresses) {
13+
const expectedTraces = [
14+
{
15+
attributes: {
16+
'http.method': 'GET',
17+
'http.url': `http://localhost:${port}/`,
18+
},
19+
end_reason: binding.kSpanEndOk,
20+
name: 'HTTP GET',
21+
parentId: '0000000000000000',
22+
thread_id: 0,
23+
kind: binding.kClient,
24+
type: binding.kSpanHttpClient,
25+
events: [
26+
{
27+
attributes: {
28+
'exception.message': 'The pending stream has been canceled (caused by: The operation was aborted)',
29+
'exception.type': 'ERR_HTTP2_STREAM_CANCEL',
30+
},
31+
name: 'exception',
32+
},
33+
],
34+
status: {
35+
code: 2, // ERROR
36+
message: 'The pending stream has been canceled (caused by: The operation was aborted)',
37+
},
38+
},
39+
{
40+
attributes: {
41+
'dns.address': [{ address: addresses[0].address,
42+
family: addresses[0].family }],
43+
'dns.hostname': 'localhost',
44+
'dns.op_type': binding.kDnsLookup,
45+
},
46+
end_reason: binding.kSpanEndOk,
47+
name: 'DNS lookup',
48+
thread_id: 0,
49+
kind: binding.kClient,
50+
type: binding.kSpanDns,
51+
},
52+
];
53+
54+
checkTracesOnExit(binding, expectedTraces);
55+
}
56+
57+
setupNSolid(common.mustSucceed(({ addresses }) => {
58+
const server = http2.createServer();
59+
const controller = new AbortController();
60+
61+
server.on('stream', common.mustNotCall());
62+
63+
server.listen(0, common.mustCall(() => {
64+
const port = server.address().port;
65+
setupTracesCheck(port, addresses);
66+
const { signal } = controller;
67+
const client = http2.connect(`http://localhost:${port}`, { signal });
68+
client.on('close', common.mustCall());
69+
70+
client.on('error', common.mustCall());
71+
72+
const req = client.request({}, {});
73+
74+
req.on('error', common.mustCall());
75+
req.on('close', common.mustCall(() => {
76+
server.close();
77+
}));
78+
79+
controller.abort();
80+
}));
81+
}));

0 commit comments

Comments
 (0)