UNPKG

@ensnode/ponder-metadata

Version:

A Hono middleware for making Ponder app metadata available to clients.

309 lines (306 loc) 9.82 kB
// src/middleware.ts import { HTTPException } from "hono/http-exception"; // src/db-helpers.ts import { pgSchema, pgTable } from "drizzle-orm/pg-core"; import { eq } from "ponder"; var getPonderMetaTableSchema = (databaseNamespace) => { if (databaseNamespace === "public") { return pgTable("_ponder_meta", (t) => ({ key: t.text().primaryKey().$type(), value: t.jsonb().$type().notNull() })); } return pgSchema(databaseNamespace).table("_ponder_meta", (t) => ({ key: t.text().primaryKey().$type(), value: t.jsonb().$type().notNull() })); }; async function queryPonderMeta(namespace, db) { const PONDER_META = getPonderMetaTableSchema(namespace); const [ponderAppMeta] = await db.select({ value: PONDER_META.value }).from(PONDER_META).where(eq(PONDER_META.key, "app")).limit(1); if (!ponderAppMeta) { throw new Error("Ponder metadata not found"); } return ponderAppMeta.value; } // src/prometheus-metrics.ts import parsePrometheusTextFormat from "parse-prometheus-text-format"; function parsePrometheusText(text) { return parsePrometheusTextFormat(text).map((metric) => ({ name: metric.name, help: metric.help || "", type: metric.type.toLowerCase(), metrics: metric.metrics.map((m) => ({ value: Number(m.value), ...m.labels && Object.keys(m.labels).length > 0 ? { labels: m.labels } : {} })) })); } var PrometheusMetrics = class _PrometheusMetrics { constructor(metrics) { this.metrics = metrics; } static parse(maybePrometheusMetricsText) { return new _PrometheusMetrics(parsePrometheusText(maybePrometheusMetricsText)); } /** * Gets all metrics of a specific name * @param name Metric name * @returns Array of metrics or undefined if not found * @example * ```ts * const metrics = parser.get('ponder_historical_total_indexing_seconds'); * // Returns: [ * // { value: 251224935, labels: { network: "1" } }, * // { value: 251224935, labels: { network: "8453" } } * // ] * ``` */ get(name) { const metric = this.metrics.find((m) => m.name === name); return metric == null ? void 0 : metric.metrics; } /** * Gets a single metric value, optionally filtered by labels * @param name Metric name * @param labelFilter Optional label key-value pairs to match * @returns Metric value or undefined if not found * @example * ```ts * // Get simple value * parser.getValue('ponder_historical_start_timestamp_seconds') // Returns: 1740391265 * * // Get value with label filter * parser.getValue('ponder_historical_total_indexing_seconds', { network: '1' }) // Returns: 251224935 * ``` */ getValue(name, labelFilter) { var _a; const metrics = this.get(name); if (!metrics || metrics.length === 0) { return void 0; } if (!labelFilter) { return (_a = metrics[0]) == null ? void 0 : _a.value; } const metric = metrics.find( (m) => m.labels && Object.entries(labelFilter).every(([k, v]) => { var _a2; return ((_a2 = m.labels) == null ? void 0 : _a2[k]) === v; }) ); return metric == null ? void 0 : metric.value; } /** * Gets a label value from a metric * @param name Metric name * @param label Label name to retrieve * @returns Label value or undefined if not found * @example * ```ts * parser.getLabel('ponder_version_info', 'version') // Returns: "0.9.18" * parser.getLabel('ponder_settings_info', 'ordering') // Returns: "omnichain" * ``` */ getLabel(name, label) { return this.getLabels(name, label)[0]; } /** * Gets all unique label values for a metric * @param name Metric name * @param label Label name to retrieve * @returns Array of unique label values * @example * ```ts * // Get all network IDs * parser.getLabels('ponder_historical_total_indexing_seconds', 'network') * // Returns: ['1', '8453'] * ``` */ getLabels(name, label) { const metrics = this.get(name); if (!metrics) return []; return [ ...new Set(metrics.map((m) => { var _a; return (_a = m.labels) == null ? void 0 : _a[label]; }).filter((v) => v !== void 0)) ]; } /** * Gets help text for a metric * @param name Metric name * @returns Help text or undefined if not found * @example * ```ts * parser.getHelp('ponder_historical_start_timestamp_seconds') * // Returns: "Timestamp at which historical indexing started" * ``` */ getHelp(name) { var _a; return (_a = this.metrics.find((m) => m.name === name)) == null ? void 0 : _a.help; } /** * Gets metric type * @param name Metric name * @returns Metric type or undefined if not found * @example * ```ts * parser.getType('ponder_version_info') // Returns: "gauge" * parser.getType('ponder_postgres_query_total') // Returns: "counter" * ``` */ getType(name) { var _a; return (_a = this.metrics.find((m) => m.name === name)) == null ? void 0 : _a.type; } /** * Gets all metric names * @returns Array of metric names * @example * ```ts * parser.getMetricNames() * // Returns: [ * // 'ponder_version_info', * // 'ponder_settings_info', * // 'ponder_historical_start_timestamp_seconds', * // 'ponder_historical_total_indexing_seconds' * // ] * ``` */ getMetricNames() { return this.metrics.map((m) => m.name); } }; // src/middleware.ts function ponderMetadata({ app, db, env, query, publicClients }) { return async function ponderMetadataMiddleware(ctx) { const indexedChainNames = Object.keys(publicClients); const ponderStatus = await query.ponderStatus(); const metrics = PrometheusMetrics.parse(await query.prometheusMetrics()); const chainIndexingStatuses = {}; for (const indexedChainName of indexedChainNames) { const publicClient = publicClients[indexedChainName]; if (!publicClient || typeof publicClient.chain === "undefined") { throw new HTTPException(500, { message: `No public client found for "${indexedChainName}" chain name` }); } const publicClientChainId = publicClient.chain.id; const fetchBlockMetadata = async (blockNumber) => { const block = await publicClient.getBlock({ blockNumber: BigInt(blockNumber) }); if (!block) { throw new Error( `Failed to fetch block metadata for block number ${blockNumber} on chain ID "${publicClientChainId}"` ); } return { number: Number(block.number), timestamp: Number(block.timestamp) }; }; const latestSafeBlockData = await publicClient.getBlock(); if (!latestSafeBlockData) { throw new HTTPException(500, { message: `Failed to fetch latest safe block for chain ID "${publicClientChainId}"` }); } const latestSafeBlock = { number: Number(latestSafeBlockData.number), timestamp: Number(latestSafeBlockData.timestamp) }; const chain = indexedChainName; const lastSyncedBlockHeight = metrics.getValue("ponder_sync_block", { chain }); let lastSyncedBlock = null; if (lastSyncedBlockHeight) { try { lastSyncedBlock = await fetchBlockMetadata(lastSyncedBlockHeight); } catch (error) { console.error("Failed to fetch block metadata for last synced block", error); } } const firstBlockToIndex = await query.firstBlockToIndexByChainId( publicClientChainId, publicClient ); const ponderStatusForChain = Object.values(ponderStatus).find( (ponderStatusEntry) => ponderStatusEntry.id === publicClientChainId ); let lastIndexedBlock = null; if (ponderStatusForChain) { if (firstBlockToIndex.number < ponderStatusForChain.block.number) { lastIndexedBlock = ponderStatusForChain.block; } } chainIndexingStatuses[publicClientChainId] = { chainId: publicClientChainId, lastSyncedBlock, lastIndexedBlock, latestSafeBlock, firstBlockToIndex }; } let ponderAppBuildId; try { ponderAppBuildId = (await queryPonderMeta(env.DATABASE_SCHEMA, db)).build_id; } catch (error) { console.error("Failed to fetch ponder metadata", error); } let ensRainbowVersionInfo = void 0; if (query.ensRainbowVersion) { try { ensRainbowVersionInfo = await query.ensRainbowVersion(); } catch (error) { console.error("Failed to fetch ENSRainbow version", error); } } const response = { app, deps: { ponder: formatTextMetricValue(metrics.getLabel("ponder_version_info", "version")), nodejs: formatTextMetricValue(metrics.getLabel("nodejs_version_info", "version")) }, env, runtime: { codebaseBuildId: formatTextMetricValue(ponderAppBuildId), chainIndexingStatuses, ensRainbow: ensRainbowVersionInfo } }; validateResponse(response); return ctx.json(response); }; } function validateResponse(response) { const { chainIndexingStatuses } = response.runtime; if (Object.keys(chainIndexingStatuses).length === 0) { throw new HTTPException(500, { message: "No chain indexing status found" }); } if (Object.values(chainIndexingStatuses).some((n) => n.firstBlockToIndex === null)) { throw new HTTPException(500, { message: "Failed to fetch first block to index for some chains" }); } } function formatTextMetricValue(value) { return value ?? "unknown"; } export { PrometheusMetrics, ponderMetadata, queryPonderMeta }; //# sourceMappingURL=index.js.map