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
JavaScript
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