-
Notifications
You must be signed in to change notification settings - Fork 851
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce internal http client
- Loading branch information
1 parent
d1f9594
commit 1df055e
Showing
32 changed files
with
1,472 additions
and
278 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
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
105 changes: 105 additions & 0 deletions
105
packages/opentelemetry-core/src/internal/http-client.ts
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,105 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export interface RequestInit { | ||
/** | ||
* Defaults to "POST". Some http clients may not support methods orther than "POST", like 'sendBeacon'. | ||
*/ | ||
method?: string; | ||
/** | ||
* Content type of the payload. Would be appended to request headers if the http client supports it. | ||
*/ | ||
contentType?: string; | ||
/** | ||
* Request headers. | ||
*/ | ||
headers?: Record<string, string>; | ||
/** | ||
* Suggestive option, this may not be supported by every http client. After the specified milliseconds the request should timeout. | ||
*/ | ||
timeoutMs?: number; | ||
} | ||
|
||
export class RetriableError extends Error { | ||
override name = 'RetriableError'; | ||
/** | ||
* @param retryAfterMillis a non-negative number indicating the milliseconds to delay after the response is received. | ||
* If it is -1, it indicates delaying with exponential backoff. | ||
* @param cause the original error. | ||
*/ | ||
constructor(public retryAfterMillis: number, public cause?: Error) { | ||
super(cause?.message); | ||
} | ||
} | ||
|
||
export class HttpClientError extends Error { | ||
override name = 'HttpClientError'; | ||
constructor( | ||
public statusMessage?: string, | ||
public statusCode?: number, | ||
public payload?: unknown | ||
) { | ||
super(statusMessage); | ||
} | ||
} | ||
|
||
export function isRetriableError(error: unknown): error is RetriableError { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return ( | ||
error != null && | ||
typeof (error as RetriableError).retryAfterMillis === 'number' | ||
); | ||
} | ||
|
||
const RETRIABLE_STATUS_CODES = [429, 502, 503, 504]; | ||
export function isRetriableStatusCode(status?: number): boolean { | ||
return status != null && RETRIABLE_STATUS_CODES.includes(status); | ||
} | ||
|
||
export function parseRetryAfterToMills(retryAfter?: string | null): number { | ||
if (retryAfter == null) { | ||
return -1; | ||
} | ||
const seconds = Number.parseInt(retryAfter, 10); | ||
if (Number.isInteger(seconds)) { | ||
return seconds > 0 ? seconds * 1000 : -1; | ||
} | ||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#directives | ||
const delay = new Date(retryAfter).getTime() - Date.now(); | ||
if (delay >= 0) { | ||
return delay * 1000; | ||
} | ||
return 0; | ||
} | ||
|
||
export type BufferLike = Uint8Array | string; | ||
|
||
export type HttpClient = | ||
| 'XMLHttpReuqest' | ||
| 'fetch' | ||
| 'node:http' | ||
| 'sendBeacon'; | ||
|
||
/** | ||
* Maximum compatible http request function that can be built on top of various http client ({@link HttpClient}). | ||
* The returned promise can be rejected by {@link RetriableError} and the initiator should try to retry the request | ||
* after backoffMs. | ||
*/ | ||
export type RequestFunction = ( | ||
url: string, | ||
payload: BufferLike, | ||
requestInit?: RequestInit | ||
) => Promise<void>; |
62 changes: 62 additions & 0 deletions
62
packages/opentelemetry-core/src/internal/http-clients/fetch.ts
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,62 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { | ||
HttpClientError, | ||
isRetriableStatusCode, | ||
parseRetryAfterToMills, | ||
RequestFunction, | ||
RetriableError, | ||
} from '../http-client'; | ||
|
||
export const isFetchRequestAvailable = () => { | ||
return typeof fetch === 'function'; | ||
}; | ||
export const fetchRequest: RequestFunction = (url, payload, requestInit) => { | ||
const abortController = new AbortController(); | ||
let timeout: ReturnType<typeof setTimeout> | undefined; | ||
if (requestInit?.timeoutMs) { | ||
timeout = setTimeout(() => { | ||
abortController.abort(); | ||
}, requestInit?.timeoutMs); | ||
} | ||
|
||
return fetch(url, { | ||
body: payload, | ||
method: requestInit?.method, | ||
headers: requestInit?.headers, | ||
signal: abortController.signal, | ||
}).then( | ||
response => { | ||
clearTimeout(timeout); | ||
if (response.status >= 200 && response.status <= 299) { | ||
return; | ||
} | ||
const error = new HttpClientError(response.statusText, response.status); | ||
if (isRetriableStatusCode(response.status)) { | ||
throw new RetriableError( | ||
parseRetryAfterToMills(response.headers.get('Retry-After')), | ||
error | ||
); | ||
} | ||
throw error; | ||
}, | ||
error => { | ||
clearTimeout(timeout); | ||
throw error; | ||
} | ||
); | ||
}; |
142 changes: 142 additions & 0 deletions
142
packages/opentelemetry-core/src/internal/http-export.ts
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,142 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { diag } from '@opentelemetry/api'; | ||
import { ExportResult, ExportResultCode } from '../ExportResult'; | ||
import { | ||
BufferLike, | ||
HttpClient, | ||
isRetriableError, | ||
RequestFunction, | ||
RequestInit, | ||
} from './http-client'; | ||
import { fetchRequest, isFetchRequestAvailable } from './http-clients/fetch'; | ||
import { internal } from '../platform'; | ||
|
||
export interface ExportOptions extends RequestInit { | ||
/** | ||
* Defaults to 0. | ||
*/ | ||
maxAttempts?: number; | ||
initialBackoff?: number; | ||
maxBackoff?: number; | ||
backoffMultiplier?: number; | ||
} | ||
|
||
const DEFAULT_EXPORT_MAX_ATTEMPTS = 5; | ||
const DEFAULT_EXPORT_INITIAL_BACKOFF = 1000; | ||
const DEFAULT_EXPORT_MAX_BACKOFF = 5000; | ||
const DEFAULT_EXPORT_BACKOFF_MULTIPLIER = 1.5; | ||
|
||
const clients = { | ||
fetch: [isFetchRequestAvailable, fetchRequest], | ||
'node:http': [internal.isHttpRequestAvailable, internal.httpRequest], | ||
XMLHttpReuqest: [internal.isXhrRequestAvailable, internal.xhrRequest], | ||
sendBeacon: [ | ||
internal.isSendBeaconRequestAvailable, | ||
internal.sendBeaconRequest, | ||
], | ||
} as const; | ||
export function determineClient( | ||
preferredClients: HttpClient[] | ||
): RequestFunction { | ||
for (let idx = 0; idx < preferredClients.length; idx++) { | ||
const name = preferredClients[idx]; | ||
if (!(name in clients)) { | ||
diag.error(`Http client "${name}" is not recognizable.`); | ||
continue; | ||
} | ||
const [test, request] = clients[name]; | ||
if (!test()) { | ||
diag.error(`Http client "${name}" is not available.`); | ||
continue; | ||
} | ||
return request; | ||
} | ||
throw new Error(`No http client available: ${preferredClients}`); | ||
} | ||
|
||
export type HttpExportClient = ( | ||
url: string, | ||
payload: BufferLike, | ||
options?: ExportOptions | ||
) => Promise<ExportResult>; | ||
|
||
/** | ||
* Creates a http-client based on the preference of the underlying methods. | ||
* The abstract http-client only exposes necessary information to the exporters | ||
* in order to broaden the compatibility across platforms. | ||
*/ | ||
export function createHttpExportClient( | ||
preferredClients: HttpClient[] | ||
): HttpExportClient { | ||
const request = determineClient(preferredClients); | ||
|
||
/** | ||
* Wraps common retrying process with http clients. | ||
*/ | ||
return function httpExport(url, payload, options) { | ||
let attemptCount = 0; | ||
const maxAttempt = options?.maxAttempts ?? DEFAULT_EXPORT_MAX_ATTEMPTS; | ||
const maxBackoff = options?.maxBackoff ?? DEFAULT_EXPORT_MAX_BACKOFF; | ||
const initialBackoff = | ||
options?.initialBackoff ?? DEFAULT_EXPORT_INITIAL_BACKOFF; | ||
const backoffMultiplier = | ||
options?.backoffMultiplier ?? DEFAULT_EXPORT_BACKOFF_MULTIPLIER; | ||
|
||
let lastBackoff = initialBackoff; | ||
|
||
const okHandler = (): ExportResult => { | ||
return { | ||
code: ExportResultCode.SUCCESS, | ||
}; | ||
}; | ||
const errorHandler = ( | ||
err: unknown | ||
): Promise<ExportResult> | ExportResult => { | ||
if (!isRetriableError(err) || attemptCount >= maxAttempt) { | ||
return { | ||
code: ExportResultCode.FAILED, | ||
error: err as Error, | ||
}; | ||
} | ||
|
||
let delay: number; | ||
if (err.retryAfterMillis >= 0) { | ||
delay = err.retryAfterMillis; | ||
} else { | ||
delay = | ||
Math.round(Math.random() * (maxBackoff - lastBackoff) + lastBackoff) * | ||
backoffMultiplier; | ||
} | ||
|
||
lastBackoff = delay; | ||
attemptCount++; | ||
return setTimeoutPromise(err.retryAfterMillis) | ||
.then(() => request(url, payload, options)) | ||
.then(okHandler, errorHandler); | ||
}; | ||
const p = request(url, payload, options).then(okHandler, errorHandler); | ||
|
||
return p; | ||
}; | ||
} | ||
|
||
function setTimeoutPromise(timeoutMs: number): Promise<void> { | ||
return new Promise(resolve => { | ||
setTimeout(resolve, timeoutMs); | ||
}); | ||
} |
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,30 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { _export } from './exporter'; | ||
import { HttpClient } from './http-client'; | ||
import { | ||
createHttpExportClient, | ||
HttpExportClient, | ||
ExportOptions, | ||
} from './http-export'; | ||
export { | ||
_export, | ||
createHttpExportClient, | ||
HttpClient, | ||
HttpExportClient, | ||
ExportOptions, | ||
}; |
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
Oops, something went wrong.