apitally
Version:
Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.
1 lines • 16.9 kB
Source Map (JSON)
{"version":3,"sources":["../../src/common/client.ts"],"sourcesContent":["import fetchRetry from \"fetch-retry\";\nimport { randomUUID } from \"node:crypto\";\n\nimport ConsumerRegistry from \"./consumerRegistry.js\";\nimport { Logger, getLogger } from \"./logging.js\";\nimport { isValidClientId, isValidEnv } from \"./paramValidation.js\";\nimport RequestCounter from \"./requestCounter.js\";\nimport RequestLogger from \"./requestLogger.js\";\nimport { getCpuMemoryUsage } from \"./resources.js\";\nimport ServerErrorCounter from \"./serverErrorCounter.js\";\nimport {\n ApitallyConfig,\n StartupData,\n StartupPayload,\n SyncPayload,\n} from \"./types.js\";\nimport ValidationErrorCounter from \"./validationErrorCounter.js\";\n\nconst SYNC_INTERVAL = 60000; // 60 seconds\nconst INITIAL_SYNC_INTERVAL = 10000; // 10 seconds\nconst INITIAL_SYNC_INTERVAL_DURATION = 3600000; // 1 hour\nconst MAX_QUEUE_TIME = 3.6e6; // 1 hour\n\nclass HTTPError extends Error {\n public response: Response;\n\n constructor(response: Response) {\n const reason = response.status\n ? `status code ${response.status}`\n : \"an unknown error\";\n super(`Request failed with ${reason}`);\n this.response = response;\n }\n}\n\nexport class ApitallyClient {\n private clientId: string;\n private env: string;\n\n private static instance?: ApitallyClient;\n private instanceUuid: string;\n private syncDataQueue: SyncPayload[];\n private syncIntervalId?: NodeJS.Timeout;\n public startupData?: StartupData;\n private startupDataSent: boolean = false;\n private enabled: boolean = true;\n\n public requestCounter: RequestCounter;\n public requestLogger: RequestLogger;\n public validationErrorCounter: ValidationErrorCounter;\n public serverErrorCounter: ServerErrorCounter;\n public consumerRegistry: ConsumerRegistry;\n public logger: Logger;\n\n constructor({\n clientId,\n env = \"dev\",\n requestLogging,\n requestLoggingConfig,\n logger,\n }: ApitallyConfig) {\n if (ApitallyClient.instance) {\n throw new Error(\"Apitally client is already initialized\");\n }\n\n this.logger = logger ?? getLogger();\n\n if (!isValidClientId(clientId)) {\n this.logger.error(\n `Invalid Apitally client ID '${clientId}' (expecting hexadecimal UUID format)`,\n );\n this.enabled = false;\n }\n if (!isValidEnv(env)) {\n this.logger.error(\n `Invalid Apitally env '${env}' (expecting 1-32 alphanumeric characters and hyphens only)`,\n );\n this.enabled = false;\n }\n if (requestLoggingConfig && !requestLogging) {\n console.warn(\n \"requestLoggingConfig is deprecated, use requestLogging instead.\",\n );\n }\n\n ApitallyClient.instance = this;\n this.clientId = clientId;\n this.env = env;\n this.instanceUuid = randomUUID();\n this.syncDataQueue = [];\n this.requestCounter = new RequestCounter();\n this.requestLogger = new RequestLogger(\n requestLogging ?? requestLoggingConfig,\n );\n this.validationErrorCounter = new ValidationErrorCounter();\n this.serverErrorCounter = new ServerErrorCounter();\n this.consumerRegistry = new ConsumerRegistry();\n this.handleShutdown = this.handleShutdown.bind(this);\n }\n\n public static getInstance() {\n if (!ApitallyClient.instance) {\n throw new Error(\"Apitally client is not initialized\");\n }\n return ApitallyClient.instance;\n }\n\n public isEnabled() {\n return this.enabled;\n }\n\n public static async shutdown() {\n if (ApitallyClient.instance) {\n await ApitallyClient.instance.handleShutdown();\n }\n }\n\n public async handleShutdown() {\n this.enabled = false;\n this.stopSync();\n await this.sendSyncData();\n await this.sendLogData();\n await this.requestLogger.close();\n ApitallyClient.instance = undefined;\n }\n\n private getHubUrlPrefix() {\n const baseURL =\n process.env.APITALLY_HUB_BASE_URL || \"https://hub.apitally.io\";\n const version = \"v2\";\n return `${baseURL}/${version}/${this.clientId}/${this.env}/`;\n }\n\n private async sendData(url: string, payload: any) {\n const fetchWithRetry = fetchRetry(fetch, {\n retries: 3,\n retryDelay: 1000,\n retryOn: [408, 429, 500, 502, 503, 504],\n });\n const response = await fetchWithRetry(this.getHubUrlPrefix() + url, {\n method: \"POST\",\n body: JSON.stringify(payload),\n headers: { \"Content-Type\": \"application/json\" },\n });\n if (!response.ok) {\n throw new HTTPError(response);\n }\n }\n\n public startSync() {\n if (!this.enabled) {\n return;\n }\n this.sync();\n this.syncIntervalId = setInterval(() => {\n this.sync();\n }, INITIAL_SYNC_INTERVAL);\n setTimeout(() => {\n if (this.syncIntervalId) {\n clearInterval(this.syncIntervalId);\n this.syncIntervalId = setInterval(() => {\n this.sync();\n }, SYNC_INTERVAL);\n }\n }, INITIAL_SYNC_INTERVAL_DURATION);\n }\n\n private async sync() {\n try {\n const promises = [this.sendSyncData(), this.sendLogData()];\n if (!this.startupDataSent) {\n promises.push(this.sendStartupData());\n }\n await Promise.all(promises);\n } catch (error) {\n this.logger.error(\"Error while syncing with Apitally Hub\", {\n error,\n });\n }\n }\n\n private stopSync() {\n if (this.syncIntervalId) {\n clearInterval(this.syncIntervalId);\n this.syncIntervalId = undefined;\n }\n }\n\n public setStartupData(data: StartupData) {\n this.startupData = data;\n this.startupDataSent = false;\n }\n\n private async sendStartupData() {\n if (this.startupData) {\n this.logger.debug(\"Sending startup data to Apitally Hub\");\n const payload: StartupPayload = {\n instance_uuid: this.instanceUuid,\n message_uuid: randomUUID(),\n ...this.startupData,\n };\n try {\n await this.sendData(\"startup\", payload);\n this.startupDataSent = true;\n } catch (error) {\n const handled = this.handleHubError(error);\n if (!handled) {\n this.logger.error((error as Error).message);\n this.logger.debug(\n \"Error while sending startup data to Apitally Hub (will retry)\",\n { error },\n );\n }\n }\n }\n }\n\n private async sendSyncData() {\n this.logger.debug(\"Synchronizing data with Apitally Hub\");\n const newPayload: SyncPayload = {\n timestamp: Date.now() / 1000,\n instance_uuid: this.instanceUuid,\n message_uuid: randomUUID(),\n requests: this.requestCounter.getAndResetRequests(),\n validation_errors:\n this.validationErrorCounter.getAndResetValidationErrors(),\n server_errors: this.serverErrorCounter.getAndResetServerErrors(),\n consumers: this.consumerRegistry.getAndResetUpdatedConsumers(),\n resources: getCpuMemoryUsage(),\n };\n this.syncDataQueue.push(newPayload);\n\n let i = 0;\n while (this.syncDataQueue.length > 0) {\n const payload = this.syncDataQueue.shift();\n if (payload) {\n try {\n if (Date.now() - payload.timestamp * 1000 <= MAX_QUEUE_TIME) {\n if (i > 0) {\n await this.randomDelay();\n }\n await this.sendData(\"sync\", payload);\n i += 1;\n }\n } catch (error) {\n const handled = this.handleHubError(error);\n if (!handled) {\n this.logger.debug(\n \"Error while synchronizing data with Apitally Hub (will retry)\",\n { error },\n );\n this.syncDataQueue.push(payload);\n break;\n }\n }\n }\n }\n }\n\n private async sendLogData() {\n this.logger.debug(\"Sending request log data to Apitally Hub\");\n await this.requestLogger.rotateFile();\n\n const fetchWithRetry = fetchRetry(fetch, {\n retries: 3,\n retryDelay: 1000,\n retryOn: [408, 429, 500, 502, 503, 504],\n });\n\n let i = 0;\n let logFile;\n while ((logFile = this.requestLogger.getFile())) {\n if (i > 0) {\n await this.randomDelay();\n }\n\n try {\n const response = await fetchWithRetry(\n `${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`,\n {\n method: \"POST\",\n body: (await logFile.getContent()) as any,\n },\n );\n\n if (response.status === 402 && response.headers.has(\"Retry-After\")) {\n const retryAfter = parseInt(\n response.headers.get(\"Retry-After\") ?? \"0\",\n );\n if (retryAfter > 0) {\n this.requestLogger.suspendUntil = Date.now() + retryAfter * 1000;\n this.requestLogger.clear();\n return;\n }\n }\n\n if (!response.ok) {\n throw new HTTPError(response);\n }\n\n logFile.delete();\n } catch (error) {\n this.requestLogger.retryFileLater(logFile);\n break;\n }\n\n i++;\n if (i >= 10) break;\n }\n }\n\n private handleHubError(error: unknown) {\n if (error instanceof HTTPError) {\n if (error.response.status === 404) {\n this.logger.error(`Invalid Apitally client ID: '${this.clientId}'`);\n this.enabled = false;\n this.stopSync();\n return true;\n }\n if (error.response.status === 422) {\n this.logger.error(\"Received validation error from Apitally Hub\");\n return true;\n }\n }\n return false;\n }\n\n private async randomDelay() {\n const delay = 100 + Math.random() * 400;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n}\n"],"mappings":"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","names":["SYNC_INTERVAL","INITIAL_SYNC_INTERVAL","INITIAL_SYNC_INTERVAL_DURATION","MAX_QUEUE_TIME","HTTPError","Error","response","reason","status","ApitallyClient","clientId","env","instanceUuid","syncDataQueue","syncIntervalId","startupData","startupDataSent","enabled","requestCounter","requestLogger","validationErrorCounter","serverErrorCounter","consumerRegistry","logger","requestLogging","requestLoggingConfig","instance","getLogger","isValidClientId","error","isValidEnv","console","warn","randomUUID","RequestCounter","RequestLogger","ValidationErrorCounter","ServerErrorCounter","ConsumerRegistry","handleShutdown","bind","getInstance","isEnabled","shutdown","stopSync","sendSyncData","sendLogData","close","undefined","getHubUrlPrefix","baseURL","process","APITALLY_HUB_BASE_URL","version","sendData","url","payload","fetchWithRetry","fetchRetry","fetch","retries","retryDelay","retryOn","method","body","JSON","stringify","headers","ok","startSync","sync","setInterval","setTimeout","clearInterval","promises","push","sendStartupData","Promise","all","setStartupData","data","debug","instance_uuid","message_uuid","handled","handleHubError","message","newPayload","timestamp","Date","now","requests","getAndResetRequests","validation_errors","getAndResetValidationErrors","server_errors","getAndResetServerErrors","consumers","getAndResetUpdatedConsumers","resources","getCpuMemoryUsage","i","length","shift","randomDelay","rotateFile","logFile","getFile","uuid","getContent","has","retryAfter","parseInt","get","suspendUntil","clear","delete","retryFileLater","delay","Math","random","resolve"]}