Skip to content

Commit 3e2a781

Browse files
committed
Add Node.js diagnostic channel support
Fixes #1792
1 parent 6dd7574 commit 3e2a781

File tree

6 files changed

+629
-0
lines changed

6 files changed

+629
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
[> Back to homepage](../readme.md#documentation)
2+
3+
## Diagnostics Channel
4+
5+
Got integrates with Node.js [diagnostic channels](https://nodejs.org/api/diagnostics_channel.html) for low-overhead observability. Events are only published when subscribers exist.
6+
7+
### Channels
8+
9+
#### `got:request:create`
10+
11+
Emitted when a request is created.
12+
13+
```ts
14+
{requestId: string, url: string, method: string}
15+
```
16+
17+
#### `got:request:start`
18+
19+
Emitted before the native HTTP request is sent.
20+
21+
```ts
22+
{requestId: string, url: string, method: string, headers: Record<string, string | string[] | undefined>}
23+
```
24+
25+
#### `got:response:start`
26+
27+
Emitted when response headers are received.
28+
29+
```ts
30+
{requestId: string, url: string, statusCode: number, headers: Record<string, string | string[] | undefined>, isFromCache: boolean}
31+
```
32+
33+
#### `got:response:end`
34+
35+
Emitted when the response completes.
36+
37+
```ts
38+
{requestId: string, url: string, statusCode: number, bodySize?: number, timings?: Timings}
39+
```
40+
41+
#### `got:request:retry`
42+
43+
Emitted when retrying a request.
44+
45+
```ts
46+
{requestId: string, retryCount: number, error: RequestError, delay: number}
47+
```
48+
49+
#### `got:request:error`
50+
51+
Emitted when a request fails.
52+
53+
```ts
54+
{requestId: string, url: string, error: RequestError, timings?: Timings}
55+
```
56+
57+
#### `got:response:redirect`
58+
59+
Emitted when following a redirect.
60+
61+
```ts
62+
{requestId: string, fromUrl: string, toUrl: string, statusCode: number}
63+
```
64+
65+
### Example
66+
67+
```js
68+
import diagnosticsChannel from 'node:diagnostics_channel';
69+
70+
const channel = diagnosticsChannel.channel('got:request:start');
71+
72+
channel.subscribe(message => {
73+
console.log(`${message.method} ${message.url}`);
74+
});
75+
```
76+
77+
All events for a single request share the same `requestId`.
78+
79+
### TypeScript
80+
81+
All message types are exported from the main package:
82+
83+
```ts
84+
import type {
85+
DiagnosticRequestCreate,
86+
DiagnosticRequestStart,
87+
DiagnosticResponseStart,
88+
DiagnosticResponseEnd,
89+
DiagnosticRequestRetry,
90+
DiagnosticRequestError,
91+
DiagnosticResponseRedirect,
92+
} from 'got';
93+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ By default, Got will retry on failure. To disable this option, set [`options.ret
127127

128128
#### Integration
129129

130+
- [x] [Diagnostics Channel](documentation/diagnostics-channel.md)
130131
- [x] [TypeScript support](documentation/typescript.md)
131132
- [x] [AWS](documentation/tips.md#aws)
132133
- [x] [Testing](documentation/tips.md#testing)

source/core/diagnostics-channel.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {randomUUID} from 'node:crypto';
2+
import diagnosticsChannel from 'node:diagnostics_channel';
3+
import type {Timings} from '@szmarczak/http-timer';
4+
import type {RequestError} from './errors.js';
5+
6+
const channels = {
7+
requestCreate: diagnosticsChannel.channel('got:request:create'),
8+
requestStart: diagnosticsChannel.channel('got:request:start'),
9+
responseStart: diagnosticsChannel.channel('got:response:start'),
10+
responseEnd: diagnosticsChannel.channel('got:response:end'),
11+
retry: diagnosticsChannel.channel('got:request:retry'),
12+
error: diagnosticsChannel.channel('got:request:error'),
13+
redirect: diagnosticsChannel.channel('got:response:redirect'),
14+
};
15+
16+
export type RequestId = string;
17+
18+
/**
19+
Message for the `got:request:create` diagnostic channel.
20+
21+
Emitted when a request is created.
22+
*/
23+
export type DiagnosticRequestCreate = {
24+
requestId: RequestId;
25+
url: string;
26+
method: string;
27+
};
28+
29+
/**
30+
Message for the `got:request:start` diagnostic channel.
31+
32+
Emitted before the native HTTP request is sent.
33+
*/
34+
export type DiagnosticRequestStart = {
35+
requestId: RequestId;
36+
url: string;
37+
method: string;
38+
headers: Record<string, string | string[] | undefined>;
39+
};
40+
41+
/**
42+
Message for the `got:response:start` diagnostic channel.
43+
44+
Emitted when response headers are received.
45+
*/
46+
export type DiagnosticResponseStart = {
47+
requestId: RequestId;
48+
url: string;
49+
statusCode: number;
50+
headers: Record<string, string | string[] | undefined>;
51+
isFromCache: boolean;
52+
};
53+
54+
/**
55+
Message for the `got:response:end` diagnostic channel.
56+
57+
Emitted when the response completes.
58+
*/
59+
export type DiagnosticResponseEnd = {
60+
requestId: RequestId;
61+
url: string;
62+
statusCode: number;
63+
bodySize?: number;
64+
timings?: Timings;
65+
};
66+
67+
/**
68+
Message for the `got:request:retry` diagnostic channel.
69+
70+
Emitted when retrying a request.
71+
*/
72+
export type DiagnosticRequestRetry = {
73+
requestId: RequestId;
74+
retryCount: number;
75+
error: RequestError;
76+
delay: number;
77+
};
78+
79+
/**
80+
Message for the `got:request:error` diagnostic channel.
81+
82+
Emitted when a request fails.
83+
*/
84+
export type DiagnosticRequestError = {
85+
requestId: RequestId;
86+
url: string;
87+
error: RequestError;
88+
timings?: Timings;
89+
};
90+
91+
/**
92+
Message for the `got:response:redirect` diagnostic channel.
93+
94+
Emitted when following a redirect.
95+
*/
96+
export type DiagnosticResponseRedirect = {
97+
requestId: RequestId;
98+
fromUrl: string;
99+
toUrl: string;
100+
statusCode: number;
101+
};
102+
103+
export function generateRequestId(): RequestId {
104+
return randomUUID();
105+
}
106+
107+
export function publishRequestCreate(message: DiagnosticRequestCreate): void {
108+
if (channels.requestCreate.hasSubscribers) {
109+
channels.requestCreate.publish(message);
110+
}
111+
}
112+
113+
export function publishRequestStart(message: DiagnosticRequestStart): void {
114+
if (channels.requestStart.hasSubscribers) {
115+
channels.requestStart.publish(message);
116+
}
117+
}
118+
119+
export function publishResponseStart(message: DiagnosticResponseStart): void {
120+
if (channels.responseStart.hasSubscribers) {
121+
channels.responseStart.publish(message);
122+
}
123+
}
124+
125+
export function publishResponseEnd(message: DiagnosticResponseEnd): void {
126+
if (channels.responseEnd.hasSubscribers) {
127+
channels.responseEnd.publish(message);
128+
}
129+
}
130+
131+
export function publishRetry(message: DiagnosticRequestRetry): void {
132+
if (channels.retry.hasSubscribers) {
133+
channels.retry.publish(message);
134+
}
135+
}
136+
137+
export function publishError(message: DiagnosticRequestError): void {
138+
if (channels.error.hasSubscribers) {
139+
channels.error.publish(message);
140+
}
141+
}
142+
143+
export function publishRedirect(message: DiagnosticResponseRedirect): void {
144+
if (channels.redirect.hasSubscribers) {
145+
channels.redirect.publish(message);
146+
}
147+
}

source/core/index.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ import {
4343
CacheError,
4444
AbortError,
4545
} from './errors.js';
46+
import {
47+
type RequestId,
48+
generateRequestId,
49+
publishRequestCreate,
50+
publishRequestStart,
51+
publishResponseStart,
52+
publishResponseEnd,
53+
publishRetry,
54+
publishError,
55+
publishRedirect,
56+
} from './diagnostics-channel.js';
4657

4758
type Error = NodeJS.ErrnoException;
4859

@@ -200,6 +211,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
200211
private _aborted: boolean;
201212
private _expectedContentLength?: number;
202213
private _byteCounter?: ByteCounter;
214+
private readonly _requestId: RequestId;
203215

204216
// We need this because `this._request` if `undefined` when using cache
205217
private _requestInitialized: boolean;
@@ -229,6 +241,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
229241
this.retryCount = 0;
230242

231243
this._stopRetry = noop;
244+
this._requestId = generateRequestId();
232245

233246
this.on('pipe', (source: any) => {
234247
if (source?.headers) {
@@ -254,6 +267,13 @@ export default class Request extends Duplex implements RequestEvents<Request> {
254267
}
255268

256269
this.requestUrl = this.options.url as URL;
270+
271+
// Publish request creation event
272+
publishRequestCreate({
273+
requestId: this._requestId,
274+
url: this.options.url?.toString() ?? '',
275+
method: this.options.method,
276+
});
257277
} catch (error: any) {
258278
const {options} = error as OptionsError;
259279
if (options) {
@@ -509,6 +529,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
509529
// and will detect it's a consumed stream, which is the correct behavior.
510530
}
511531

532+
// Publish retry event
533+
publishRetry({
534+
requestId: this._requestId,
535+
retryCount: this.retryCount + 1,
536+
error: typedError,
537+
delay: backoff,
538+
});
539+
512540
this.emit('retry', this.retryCount + 1, error, (updatedOptions?: OptionsInit) => {
513541
const request = new Request(options.url, updatedOptions, options);
514542
request.retryCount = this.retryCount + 1;
@@ -823,6 +851,15 @@ export default class Request extends Duplex implements RequestEvents<Request> {
823851

824852
this.response = typedResponse;
825853

854+
// Publish response start event
855+
publishResponseStart({
856+
requestId: this._requestId,
857+
url: typedResponse.url,
858+
statusCode,
859+
headers: response.headers,
860+
isFromCache: typedResponse.isFromCache,
861+
});
862+
826863
// Workaround for http-timer bug: when connecting to an IP address (no DNS lookup),
827864
// http-timer sets lookup = connect instead of lookup = socket, resulting in
828865
// dns = lookup - socket being a small positive number instead of 0.
@@ -978,6 +1015,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
9781015
await hook(updatedOptions as NormalizedOptions, typedResponse);
9791016
}
9801017

1018+
// Publish redirect event
1019+
publishRedirect({
1020+
requestId: this._requestId,
1021+
fromUrl: url!.toString(),
1022+
toUrl: redirectUrl.toString(),
1023+
statusCode,
1024+
});
1025+
9811026
this.emit('redirect', updatedOptions, typedResponse);
9821027

9831028
this.options = updatedOptions;
@@ -1027,6 +1072,16 @@ export default class Request extends Duplex implements RequestEvents<Request> {
10271072

10281073
this._responseSize = this._downloadedSize;
10291074
this.emit('downloadProgress', this.downloadProgress);
1075+
1076+
// Publish response end event
1077+
publishResponseEnd({
1078+
requestId: this._requestId,
1079+
url: typedResponse.url,
1080+
statusCode,
1081+
bodySize: this._downloadedSize,
1082+
timings: this.timings,
1083+
});
1084+
10301085
this.push(null);
10311086
});
10321087

@@ -1124,6 +1179,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11241179
const {options} = this;
11251180
const {timeout, url} = options;
11261181

1182+
// Publish request start event
1183+
publishRequestStart({
1184+
requestId: this._requestId,
1185+
url: url?.toString() ?? '',
1186+
method: options.method,
1187+
headers: options.headers,
1188+
});
1189+
11271190
timer(request);
11281191

11291192
this._cancelTimeouts = timedOut(request, timeout, url as URL);
@@ -1531,6 +1594,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
15311594
error = new RequestError(error_.message, error_, this);
15321595
}
15331596

1597+
// Publish error event
1598+
publishError({
1599+
requestId: this._requestId,
1600+
url: this.options?.url?.toString() ?? '',
1601+
error,
1602+
timings: this.timings,
1603+
});
1604+
15341605
this.destroy(error);
15351606

15361607
// Manually emit error for Promise API to ensure it receives it.

source/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './core/response.js';
2121
export type {default as Request} from './core/index.js';
2222
export * from './core/index.js';
2323
export * from './core/errors.js';
24+
export * from './core/diagnostics-channel.js';
2425
export type {Delays} from './core/timed-out.js';
2526
export {default as calculateRetryDelay} from './core/calculate-retry-delay.js';
2627
export * from './as-promise/types.js';

0 commit comments

Comments
 (0)