UNPKG

apitally

Version:

Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.

297 lines 9.2 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); var _a; import fetchRetry from "fetch-retry"; import { randomUUID } from "node:crypto"; import ConsumerRegistry from "./consumerRegistry.js"; import { getLogger } from "./logging.js"; import { isValidClientId, isValidEnv } from "./paramValidation.js"; import RequestCounter from "./requestCounter.js"; import RequestLogger from "./requestLogger.js"; import { getCpuMemoryUsage } from "./resources.js"; import ServerErrorCounter from "./serverErrorCounter.js"; import ValidationErrorCounter from "./validationErrorCounter.js"; const SYNC_INTERVAL = 6e4; const INITIAL_SYNC_INTERVAL = 1e4; const INITIAL_SYNC_INTERVAL_DURATION = 36e5; const MAX_QUEUE_TIME = 36e5; let HTTPError = (_a = class extends Error { response; constructor(response) { const reason = response.status ? `status code ${response.status}` : "an unknown error"; super(`Request failed with ${reason}`); this.response = response; } }, __name(_a, "HTTPError"), _a); const _ApitallyClient = class _ApitallyClient { clientId; env; instanceUuid; syncDataQueue; syncIntervalId; startupData; startupDataSent = false; enabled = true; requestCounter; requestLogger; validationErrorCounter; serverErrorCounter; consumerRegistry; logger; constructor({ clientId, env = "dev", requestLogging, requestLoggingConfig, logger }) { if (_ApitallyClient.instance) { throw new Error("Apitally client is already initialized"); } this.logger = logger ?? getLogger(); if (!isValidClientId(clientId)) { this.logger.error(`Invalid Apitally client ID '${clientId}' (expecting hexadecimal UUID format)`); this.enabled = false; } if (!isValidEnv(env)) { this.logger.error(`Invalid Apitally env '${env}' (expecting 1-32 alphanumeric characters and hyphens only)`); this.enabled = false; } if (requestLoggingConfig && !requestLogging) { console.warn("requestLoggingConfig is deprecated, use requestLogging instead."); } _ApitallyClient.instance = this; this.clientId = clientId; this.env = env; this.instanceUuid = randomUUID(); this.syncDataQueue = []; this.requestCounter = new RequestCounter(); this.requestLogger = new RequestLogger(requestLogging ?? requestLoggingConfig); this.validationErrorCounter = new ValidationErrorCounter(); this.serverErrorCounter = new ServerErrorCounter(); this.consumerRegistry = new ConsumerRegistry(); this.handleShutdown = this.handleShutdown.bind(this); } static getInstance() { if (!_ApitallyClient.instance) { throw new Error("Apitally client is not initialized"); } return _ApitallyClient.instance; } isEnabled() { return this.enabled; } static async shutdown() { if (_ApitallyClient.instance) { await _ApitallyClient.instance.handleShutdown(); } } async handleShutdown() { this.enabled = false; this.stopSync(); await this.sendSyncData(); await this.sendLogData(); await this.requestLogger.close(); _ApitallyClient.instance = void 0; } getHubUrlPrefix() { const baseURL = process.env.APITALLY_HUB_BASE_URL || "https://hub.apitally.io"; const version = "v2"; return `${baseURL}/${version}/${this.clientId}/${this.env}/`; } async sendData(url, payload) { const fetchWithRetry = fetchRetry(fetch, { retries: 3, retryDelay: 1e3, retryOn: [ 408, 429, 500, 502, 503, 504 ] }); const response = await fetchWithRetry(this.getHubUrlPrefix() + url, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } }); if (!response.ok) { throw new HTTPError(response); } } startSync() { if (!this.enabled) { return; } this.sync(); this.syncIntervalId = setInterval(() => { this.sync(); }, INITIAL_SYNC_INTERVAL); setTimeout(() => { if (this.syncIntervalId) { clearInterval(this.syncIntervalId); this.syncIntervalId = setInterval(() => { this.sync(); }, SYNC_INTERVAL); } }, INITIAL_SYNC_INTERVAL_DURATION); } async sync() { try { const promises = [ this.sendSyncData(), this.sendLogData() ]; if (!this.startupDataSent) { promises.push(this.sendStartupData()); } await Promise.all(promises); } catch (error) { this.logger.error("Error while syncing with Apitally Hub", { error }); } } stopSync() { if (this.syncIntervalId) { clearInterval(this.syncIntervalId); this.syncIntervalId = void 0; } } setStartupData(data) { this.startupData = data; this.startupDataSent = false; } async sendStartupData() { if (this.startupData) { this.logger.debug("Sending startup data to Apitally Hub"); const payload = { instance_uuid: this.instanceUuid, message_uuid: randomUUID(), ...this.startupData }; try { await this.sendData("startup", payload); this.startupDataSent = true; } catch (error) { const handled = this.handleHubError(error); if (!handled) { this.logger.error(error.message); this.logger.debug("Error while sending startup data to Apitally Hub (will retry)", { error }); } } } } async sendSyncData() { this.logger.debug("Synchronizing data with Apitally Hub"); const newPayload = { timestamp: Date.now() / 1e3, instance_uuid: this.instanceUuid, message_uuid: randomUUID(), requests: this.requestCounter.getAndResetRequests(), validation_errors: this.validationErrorCounter.getAndResetValidationErrors(), server_errors: this.serverErrorCounter.getAndResetServerErrors(), consumers: this.consumerRegistry.getAndResetUpdatedConsumers(), resources: getCpuMemoryUsage() }; this.syncDataQueue.push(newPayload); let i = 0; while (this.syncDataQueue.length > 0) { const payload = this.syncDataQueue.shift(); if (payload) { try { if (Date.now() - payload.timestamp * 1e3 <= MAX_QUEUE_TIME) { if (i > 0) { await this.randomDelay(); } await this.sendData("sync", payload); i += 1; } } catch (error) { const handled = this.handleHubError(error); if (!handled) { this.logger.debug("Error while synchronizing data with Apitally Hub (will retry)", { error }); this.syncDataQueue.push(payload); break; } } } } } async sendLogData() { this.logger.debug("Sending request log data to Apitally Hub"); await this.requestLogger.rotateFile(); const fetchWithRetry = fetchRetry(fetch, { retries: 3, retryDelay: 1e3, retryOn: [ 408, 429, 500, 502, 503, 504 ] }); let i = 0; let logFile; while (logFile = this.requestLogger.getFile()) { if (i > 0) { await this.randomDelay(); } try { const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, { method: "POST", body: await logFile.getContent() }); if (response.status === 402 && response.headers.has("Retry-After")) { const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0"); if (retryAfter > 0) { this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3; this.requestLogger.clear(); return; } } if (!response.ok) { throw new HTTPError(response); } logFile.delete(); } catch (error) { this.requestLogger.retryFileLater(logFile); break; } i++; if (i >= 10) break; } } handleHubError(error) { if (error instanceof HTTPError) { if (error.response.status === 404) { this.logger.error(`Invalid Apitally client ID: '${this.clientId}'`); this.enabled = false; this.stopSync(); return true; } if (error.response.status === 422) { this.logger.error("Received validation error from Apitally Hub"); return true; } } return false; } async randomDelay() { const delay = 100 + Math.random() * 400; await new Promise((resolve) => setTimeout(resolve, delay)); } }; __name(_ApitallyClient, "ApitallyClient"); __publicField(_ApitallyClient, "instance"); let ApitallyClient = _ApitallyClient; export { ApitallyClient }; //# sourceMappingURL=client.js.map