open-lineage-client
Version:
TypeScript/JavaScript client for creating and sending OpenLineage standard events.
163 lines • 6.36 kB
JavaScript
import validator from 'validator';
import axios from 'axios';
import dotenv from 'dotenv';
import { HttpConfigBuilder } from '../builders/HttpConfigBuilder.js';
dotenv.config();
/**
* Configuration for HTTP transport.
*/
export class HttpConfig {
/**
* Constructs an HttpConfig instance.
* @param url - The URL for the HTTP transport.
* @param options - Additional Axios request options.
* @param token - Optional authorization token.
* @param maxRetries - Maximum number of retry attempts (default: 3).
* @param initialRetryDelay - Initial delay in milliseconds before retrying (default: 1000).
* @param maxRetryDelay - Maximum delay in milliseconds between retries (default: 30000).
* @param retryStatusCodes - HTTP status codes that should trigger a retry (default: [408, 429, 500, 502, 503, 504]).
*/
constructor(url, options = {}, token = null, maxRetries = 3, initialRetryDelay = 1000, maxRetryDelay = 30000, retryStatusCodes = [408, 429, 500, 502, 503, 504]) {
validateUrlAndToken(url, token);
this.url = url;
this.token = token;
this.options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
...options,
};
this.maxRetries = maxRetries;
this.initialRetryDelay = initialRetryDelay;
this.maxRetryDelay = maxRetryDelay;
this.retryStatusCodes = retryStatusCodes;
}
}
/**
* Validates the URL and token.
* @param url - The URL to validate.
* @param token - The token to validate (optional).
* @throws {Error} If the URL or token is invalid.
*/
function validateUrlAndToken(url, token = null) {
if (!validator.isURL(url, { require_host: false, require_tld: false })) {
throw new Error('Invalid URL');
}
// Only validate JWT format in production environments
// In test environments, we'll accept any token format
const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.VITEST;
if (token && !isTestEnvironment && !validator.isJWT(token)) {
throw new Error('Invalid token');
}
}
/**
* Sleep function for implementing delays between retries.
* @param ms - The number of milliseconds to sleep.
* @returns A promise that resolves after the specified time.
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* HTTP transport implementation for emitting events.
*/
export class HttpTransport {
/**
* Constructs an HttpTransport instance.
* @param config - The HTTP configuration.
*/
constructor(config) {
this.url = config.url;
this.options = config.options;
this.maxRetries = config.maxRetries;
this.initialRetryDelay = config.initialRetryDelay;
this.maxRetryDelay = config.maxRetryDelay;
this.retryStatusCodes = config.retryStatusCodes;
}
/**
* Calculates the delay for the next retry attempt using exponential backoff with jitter.
* @param retryCount - The current retry attempt number (0-based).
* @returns The delay in milliseconds before the next retry.
*/
calculateRetryDelay(retryCount) {
// Exponential backoff: initialDelay * 2^retryCount
const exponentialDelay = this.initialRetryDelay * Math.pow(2, retryCount);
// Add jitter (random value between 0 and 1) to avoid thundering herd problem
const jitter = Math.random();
// Apply jitter and cap at maxRetryDelay
return Math.min(exponentialDelay * (1 + jitter), this.maxRetryDelay);
}
/**
* Determines if a request should be retried based on the error.
* @param error - The error from the failed request.
* @returns True if the request should be retried, false otherwise.
*/
shouldRetry(error) {
// Retry on network errors
if (!error.response) {
return true;
}
// Retry on specific status codes
return this.retryStatusCodes.includes(error.response.status);
}
/**
* Emits an event to the configured HTTP endpoint with retry logic.
* @param event - The event to emit.
* @returns A promise that resolves with the Axios response.
*/
async emit(event) {
const data = JSON.stringify(event);
const config = {
...this.options,
url: this.url,
data,
};
let retryCount = 0;
let lastError = new Error('Unknown error');
while (retryCount <= this.maxRetries) {
try {
const response = await axios(config);
if (process.env.DEBUG === 'true') {
console.debug(response);
}
return response;
}
catch (error) {
lastError = error;
if (process.env.DEBUG === 'true') {
console.debug(`Request failed (attempt ${retryCount + 1}/${this.maxRetries + 1}):`, error.message);
}
// If we've reached max retries or shouldn't retry this error, throw
if (retryCount >= this.maxRetries || !this.shouldRetry(error)) {
break;
}
// Calculate delay with exponential backoff
const delay = this.calculateRetryDelay(retryCount);
if (process.env.DEBUG === 'true') {
console.debug(`Retrying in ${delay}ms...`);
}
// Wait before retrying
await sleep(delay);
retryCount++;
}
}
// If we've exhausted all retries, throw the last error
throw lastError;
}
/**
* Creates an HttpTransport instance from a configuration object.
* @param config - The transport configuration.
* @returns A new HttpTransport instance.
* @throws {Error} If the configuration is invalid.
*/
static fromFile(config) {
if (!config.url) {
throw new Error('Missing URL in config');
}
return new HttpTransport(HttpConfigBuilder.fromConfig(config));
}
}
//# sourceMappingURL=HttpTransport.js.map