apitally
Version:
Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.
330 lines • 11.2 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
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 __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var client_exports = {};
__export(client_exports, {
ApitallyClient: () => ApitallyClient
});
module.exports = __toCommonJS(client_exports);
var import_fetch_retry = __toESM(require("fetch-retry"), 1);
var import_node_crypto = require("node:crypto");
var import_consumerRegistry = __toESM(require("./consumerRegistry.js"), 1);
var import_logging = require("./logging.js");
var import_paramValidation = require("./paramValidation.js");
var import_requestCounter = __toESM(require("./requestCounter.js"), 1);
var import_requestLogger = __toESM(require("./requestLogger.js"), 1);
var import_resources = require("./resources.js");
var import_serverErrorCounter = __toESM(require("./serverErrorCounter.js"), 1);
var import_validationErrorCounter = __toESM(require("./validationErrorCounter.js"), 1);
var _a;
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 ?? (0, import_logging.getLogger)();
if (!(0, import_paramValidation.isValidClientId)(clientId)) {
this.logger.error(`Invalid Apitally client ID '${clientId}' (expecting hexadecimal UUID format)`);
this.enabled = false;
}
if (!(0, import_paramValidation.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 = (0, import_node_crypto.randomUUID)();
this.syncDataQueue = [];
this.requestCounter = new import_requestCounter.default();
this.requestLogger = new import_requestLogger.default(requestLogging ?? requestLoggingConfig);
this.validationErrorCounter = new import_validationErrorCounter.default();
this.serverErrorCounter = new import_serverErrorCounter.default();
this.consumerRegistry = new import_consumerRegistry.default();
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 = (0, import_fetch_retry.default)(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: (0, import_node_crypto.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: (0, import_node_crypto.randomUUID)(),
requests: this.requestCounter.getAndResetRequests(),
validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
server_errors: this.serverErrorCounter.getAndResetServerErrors(),
consumers: this.consumerRegistry.getAndResetUpdatedConsumers(),
resources: (0, import_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 = (0, import_fetch_retry.default)(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;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ApitallyClient
});
//# sourceMappingURL=client.cjs.map