@ensnode/ponder-metadata 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/middleware.d.ts +18 -0
- package/dist/middleware.js +283 -0
- package/dist/middleware.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NameHash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MiddlewareHandler } from 'hono';
|
|
2
|
+
import { ReadonlyDrizzle } from 'ponder';
|
|
3
|
+
import { PublicClient } from 'viem';
|
|
4
|
+
|
|
5
|
+
declare function ponderMetadata({ app, db, env, fetchPrometheusMetrics, publicClients, }: {
|
|
6
|
+
db: ReadonlyDrizzle<Record<string, unknown>>;
|
|
7
|
+
app: {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
env: {
|
|
12
|
+
DATABASE_SCHEMA: string;
|
|
13
|
+
} & Record<string, unknown>;
|
|
14
|
+
fetchPrometheusMetrics: () => Promise<string>;
|
|
15
|
+
publicClients: Record<number, PublicClient>;
|
|
16
|
+
}): MiddlewareHandler;
|
|
17
|
+
|
|
18
|
+
export { ponderMetadata };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// src/db-helpers.ts
|
|
2
|
+
import { pgSchema, pgTable } from "drizzle-orm/pg-core";
|
|
3
|
+
import { eq } from "ponder";
|
|
4
|
+
var getPonderMeta = (namespace) => {
|
|
5
|
+
if (namespace === "public") {
|
|
6
|
+
return pgTable("_ponder_meta", (t) => ({
|
|
7
|
+
key: t.text().primaryKey().$type(),
|
|
8
|
+
value: t.jsonb().$type().notNull()
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
return pgSchema(namespace).table("_ponder_meta", (t) => ({
|
|
12
|
+
key: t.text().primaryKey().$type(),
|
|
13
|
+
value: t.jsonb().$type().notNull()
|
|
14
|
+
}));
|
|
15
|
+
};
|
|
16
|
+
var getPonderStatus = (namespace) => {
|
|
17
|
+
if (namespace === "public") {
|
|
18
|
+
return pgTable("_ponder_status", (t) => ({
|
|
19
|
+
network_name: t.text().primaryKey(),
|
|
20
|
+
block_number: t.bigint({ mode: "number" }),
|
|
21
|
+
block_timestamp: t.bigint({ mode: "number" }),
|
|
22
|
+
ready: t.boolean().notNull()
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
return pgSchema(namespace).table("_ponder_status", (t) => ({
|
|
26
|
+
network_name: t.text().primaryKey(),
|
|
27
|
+
block_number: t.bigint({ mode: "number" }),
|
|
28
|
+
block_timestamp: t.bigint({ mode: "number" }),
|
|
29
|
+
ready: t.boolean().notNull()
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
async function queryPonderStatus(namespace, db) {
|
|
33
|
+
const PONDER_STATUS = getPonderStatus(namespace);
|
|
34
|
+
return db.select().from(PONDER_STATUS);
|
|
35
|
+
}
|
|
36
|
+
async function queryPonderMeta(namespace, db) {
|
|
37
|
+
const PONDER_META = getPonderMeta(namespace);
|
|
38
|
+
return db.select({ value: PONDER_META.value }).from(PONDER_META).where(eq(PONDER_META.key, "app")).limit(1).then((result) => {
|
|
39
|
+
var _a;
|
|
40
|
+
return (_a = result[0]) == null ? void 0 : _a.value;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/prometheus-metrics.ts
|
|
45
|
+
import parsePrometheusTextFormat from "parse-prometheus-text-format";
|
|
46
|
+
function parsePrometheusText(text) {
|
|
47
|
+
return parsePrometheusTextFormat(text).map((metric) => ({
|
|
48
|
+
name: metric.name,
|
|
49
|
+
help: metric.help || "",
|
|
50
|
+
type: metric.type.toLowerCase(),
|
|
51
|
+
metrics: metric.metrics.map((m) => ({
|
|
52
|
+
value: Number(m.value),
|
|
53
|
+
...m.labels && Object.keys(m.labels).length > 0 ? { labels: m.labels } : {}
|
|
54
|
+
}))
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
var PrometheusMetrics = class _PrometheusMetrics {
|
|
58
|
+
constructor(metrics) {
|
|
59
|
+
this.metrics = metrics;
|
|
60
|
+
}
|
|
61
|
+
static parse(maybePrometheusMetricsText) {
|
|
62
|
+
return new _PrometheusMetrics(parsePrometheusText(maybePrometheusMetricsText));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Gets all metrics of a specific name
|
|
66
|
+
* @param name Metric name
|
|
67
|
+
* @returns Array of metrics or undefined if not found
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const metrics = parser.get('ponder_historical_total_indexing_seconds');
|
|
71
|
+
* // Returns: [
|
|
72
|
+
* // { value: 251224935, labels: { network: "1" } },
|
|
73
|
+
* // { value: 251224935, labels: { network: "8453" } }
|
|
74
|
+
* // ]
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
get(name) {
|
|
78
|
+
const metric = this.metrics.find((m) => m.name === name);
|
|
79
|
+
return metric == null ? void 0 : metric.metrics;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Gets a single metric value, optionally filtered by labels
|
|
83
|
+
* @param name Metric name
|
|
84
|
+
* @param labelFilter Optional label key-value pairs to match
|
|
85
|
+
* @returns Metric value or undefined if not found
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* // Get simple value
|
|
89
|
+
* parser.getValue('ponder_historical_start_timestamp_seconds') // Returns: 1740391265
|
|
90
|
+
*
|
|
91
|
+
* // Get value with label filter
|
|
92
|
+
* parser.getValue('ponder_historical_total_indexing_seconds', { network: '1' }) // Returns: 251224935
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
getValue(name, labelFilter) {
|
|
96
|
+
var _a;
|
|
97
|
+
const metrics = this.get(name);
|
|
98
|
+
if (!metrics || metrics.length === 0) {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
if (!labelFilter) {
|
|
102
|
+
return (_a = metrics[0]) == null ? void 0 : _a.value;
|
|
103
|
+
}
|
|
104
|
+
const metric = metrics.find(
|
|
105
|
+
(m) => m.labels && Object.entries(labelFilter).every(([k, v]) => {
|
|
106
|
+
var _a2;
|
|
107
|
+
return ((_a2 = m.labels) == null ? void 0 : _a2[k]) === v;
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
return metric == null ? void 0 : metric.value;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Gets a label value from a metric
|
|
114
|
+
* @param name Metric name
|
|
115
|
+
* @param label Label name to retrieve
|
|
116
|
+
* @returns Label value or undefined if not found
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* parser.getLabel('ponder_version_info', 'version') // Returns: "0.9.18"
|
|
120
|
+
* parser.getLabel('ponder_settings_info', 'ordering') // Returns: "omnichain"
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
getLabel(name, label) {
|
|
124
|
+
return this.getLabels(name, label)[0];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Gets all unique label values for a metric
|
|
128
|
+
* @param name Metric name
|
|
129
|
+
* @param label Label name to retrieve
|
|
130
|
+
* @returns Array of unique label values
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* // Get all network IDs
|
|
134
|
+
* parser.getLabels('ponder_historical_total_indexing_seconds', 'network')
|
|
135
|
+
* // Returns: ['1', '8453']
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
getLabels(name, label) {
|
|
139
|
+
const metrics = this.get(name);
|
|
140
|
+
if (!metrics) return [];
|
|
141
|
+
return [
|
|
142
|
+
...new Set(metrics.map((m) => {
|
|
143
|
+
var _a;
|
|
144
|
+
return (_a = m.labels) == null ? void 0 : _a[label];
|
|
145
|
+
}).filter((v) => v !== void 0))
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Gets help text for a metric
|
|
150
|
+
* @param name Metric name
|
|
151
|
+
* @returns Help text or undefined if not found
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* parser.getHelp('ponder_historical_start_timestamp_seconds')
|
|
155
|
+
* // Returns: "Timestamp at which historical indexing started"
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
getHelp(name) {
|
|
159
|
+
var _a;
|
|
160
|
+
return (_a = this.metrics.find((m) => m.name === name)) == null ? void 0 : _a.help;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Gets metric type
|
|
164
|
+
* @param name Metric name
|
|
165
|
+
* @returns Metric type or undefined if not found
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* parser.getType('ponder_version_info') // Returns: "gauge"
|
|
169
|
+
* parser.getType('ponder_postgres_query_total') // Returns: "counter"
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
getType(name) {
|
|
173
|
+
var _a;
|
|
174
|
+
return (_a = this.metrics.find((m) => m.name === name)) == null ? void 0 : _a.type;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Gets all metric names
|
|
178
|
+
* @returns Array of metric names
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* parser.getMetricNames()
|
|
182
|
+
* // Returns: [
|
|
183
|
+
* // 'ponder_version_info',
|
|
184
|
+
* // 'ponder_settings_info',
|
|
185
|
+
* // 'ponder_historical_start_timestamp_seconds',
|
|
186
|
+
* // 'ponder_historical_total_indexing_seconds'
|
|
187
|
+
* // ]
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
getMetricNames() {
|
|
191
|
+
return this.metrics.map((m) => m.name);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/middleware.ts
|
|
196
|
+
function ponderMetadata({
|
|
197
|
+
app,
|
|
198
|
+
db,
|
|
199
|
+
env,
|
|
200
|
+
fetchPrometheusMetrics,
|
|
201
|
+
publicClients
|
|
202
|
+
}) {
|
|
203
|
+
return async function ponderMetadataMiddleware(ctx) {
|
|
204
|
+
const indexedChainIds = Object.keys(publicClients).map(Number);
|
|
205
|
+
const ponderStatus = await queryPonderStatus(env.DATABASE_SCHEMA, db);
|
|
206
|
+
const ponderMeta = await queryPonderMeta(env.DATABASE_SCHEMA, db);
|
|
207
|
+
const metrics = PrometheusMetrics.parse(await fetchPrometheusMetrics());
|
|
208
|
+
const networkIndexingStatus = {};
|
|
209
|
+
for (const indexedChainId of indexedChainIds) {
|
|
210
|
+
const publicClient = publicClients[indexedChainId];
|
|
211
|
+
if (!publicClient) {
|
|
212
|
+
throw new Error(`No public client found for chainId ${indexedChainId}`);
|
|
213
|
+
}
|
|
214
|
+
const latestSafeBlock = await publicClient.getBlock();
|
|
215
|
+
const network = indexedChainId.toString();
|
|
216
|
+
const ponderStatusForNetwork = ponderStatus.find((s) => s.network_name === network);
|
|
217
|
+
const lastIndexedBlock = mapPonderStatusBlockToBlockMetadata(ponderStatusForNetwork);
|
|
218
|
+
networkIndexingStatus[network] = {
|
|
219
|
+
totalBlocksCount: metrics.getValue("ponder_historical_total_blocks", {
|
|
220
|
+
network
|
|
221
|
+
}) ?? null,
|
|
222
|
+
cachedBlocksCount: metrics.getValue("ponder_historical_cached_blocks", {
|
|
223
|
+
network
|
|
224
|
+
}) ?? null,
|
|
225
|
+
lastSyncedBlock: blockInfo({
|
|
226
|
+
number: metrics.getValue("ponder_sync_block", {
|
|
227
|
+
network
|
|
228
|
+
}) ?? 0,
|
|
229
|
+
timestamp: 0
|
|
230
|
+
}),
|
|
231
|
+
latestSafeBlock: blockInfo({
|
|
232
|
+
number: Number(latestSafeBlock.number),
|
|
233
|
+
timestamp: Number(latestSafeBlock.timestamp)
|
|
234
|
+
}),
|
|
235
|
+
lastIndexedBlock,
|
|
236
|
+
isRealtime: Boolean(metrics.getValue("ponder_sync_is_realtime", { network })),
|
|
237
|
+
isComplete: Boolean(metrics.getValue("ponder_sync_is_complete", { network })),
|
|
238
|
+
isQueued: lastIndexedBlock === null
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return ctx.json({
|
|
242
|
+
app,
|
|
243
|
+
// application dependencies version
|
|
244
|
+
deps: {
|
|
245
|
+
ponder: metrics.getLabel("ponder_version_info", "version"),
|
|
246
|
+
nodejs: metrics.getLabel("nodejs_version_info", "version")
|
|
247
|
+
},
|
|
248
|
+
// application environment variables
|
|
249
|
+
env,
|
|
250
|
+
// application runtime information
|
|
251
|
+
runtime: {
|
|
252
|
+
// application build id
|
|
253
|
+
// https://github.com/ponder-sh/ponder/blob/626e524/packages/core/src/build/index.ts#L425-L431
|
|
254
|
+
codebaseBuildId: ponderMeta == null ? void 0 : ponderMeta.build_id,
|
|
255
|
+
// tableNames: meta?.table_names,
|
|
256
|
+
networkIndexingStatus
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function blockInfo(block) {
|
|
262
|
+
if (!block.number) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
height: block.number,
|
|
267
|
+
timestamp: block.timestamp ?? 0,
|
|
268
|
+
utc: block.timestamp ? new Date(block.timestamp * 1e3).toISOString() : ""
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function mapPonderStatusBlockToBlockMetadata(block) {
|
|
272
|
+
if (!(block == null ? void 0 : block.block_number)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return blockInfo({
|
|
276
|
+
number: block.block_number,
|
|
277
|
+
timestamp: block.block_timestamp
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
ponderMetadata
|
|
282
|
+
};
|
|
283
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db-helpers.ts","../src/prometheus-metrics.ts","../src/middleware.ts"],"sourcesContent":["import { pgSchema, pgTable } from \"drizzle-orm/pg-core\";\nimport { type ReadonlyDrizzle, eq } from \"ponder\";\n\ntype NamespaceBuild = string;\n\ntype PonderApp = {\n is_locked: 0 | 1;\n is_dev: 0 | 1;\n heartbeat_at: number;\n build_id: string;\n checkpoint: string;\n table_names: string[];\n version: string;\n};\n\nconst getPonderMeta = (namespace: NamespaceBuild) => {\n if (namespace === \"public\") {\n return pgTable(\"_ponder_meta\", (t) => ({\n key: t.text().primaryKey().$type<\"app\">(),\n value: t.jsonb().$type<PonderApp>().notNull(),\n }));\n }\n\n return pgSchema(namespace).table(\"_ponder_meta\", (t) => ({\n key: t.text().primaryKey().$type<\"app\">(),\n value: t.jsonb().$type<PonderApp>().notNull(),\n }));\n};\n\nconst getPonderStatus = (namespace: NamespaceBuild) => {\n if (namespace === \"public\") {\n return pgTable(\"_ponder_status\", (t) => ({\n network_name: t.text().primaryKey(),\n block_number: t.bigint({ mode: \"number\" }),\n block_timestamp: t.bigint({ mode: \"number\" }),\n ready: t.boolean().notNull(),\n }));\n }\n\n return pgSchema(namespace).table(\"_ponder_status\", (t) => ({\n network_name: t.text().primaryKey(),\n block_number: t.bigint({ mode: \"number\" }),\n block_timestamp: t.bigint({ mode: \"number\" }),\n ready: t.boolean().notNull(),\n }));\n};\n\nexport async function queryPonderStatus(\n namespace: string,\n db: ReadonlyDrizzle<Record<string, unknown>>,\n) {\n const PONDER_STATUS = getPonderStatus(namespace);\n\n return db.select().from(PONDER_STATUS);\n}\n\nexport async function queryPonderMeta(\n namespace: string,\n db: ReadonlyDrizzle<Record<string, unknown>>,\n) {\n const PONDER_META = getPonderMeta(namespace);\n\n return db\n .select({ value: PONDER_META.value })\n .from(PONDER_META)\n .where(eq(PONDER_META.key, \"app\"))\n .limit(1)\n .then((result: any) => result[0]?.value);\n}\n","import parsePrometheusTextFormat from \"parse-prometheus-text-format\";\nimport type { PrometheusMetric } from \"parse-prometheus-text-format\";\n// Ensures local declaration file is available to downstream consumers\nimport \"./types/parse-prometheus-text-format\";\n\ninterface ParsedPrometheusMetric extends Omit<PrometheusMetric, \"metrics\"> {\n metrics: Array<{\n value: number;\n labels?: Record<string, string>;\n }>;\n}\n\n/**\n * Converts Prometheus text format to JSON format compatible with prom2json\n * @param text Raw Prometheus metric text\n * @returns Array of metrics in prom2json compatible format\n * @example\n * ```ts\n * const metrics = parsePrometheusText(`\n * # HELP ponder_version_info Ponder version information\n * # TYPE ponder_version_info gauge\n * ponder_version_info{version=\"0.9.18\",major=\"0\",minor=\"9\",patch=\"18\"} 1\n * `);\n * // Returns:\n * // [{\n * // name: \"ponder_version_info\",\n * // help: \"Ponder version information\",\n * // type: \"gauge\",\n * // metrics: [{\n * // value: 1,\n * // labels: { version: \"0.9.18\", major: \"0\", minor: \"9\", patch: \"18\" }\n * // }]\n * // }]\n * ```\n */\nexport function parsePrometheusText(text: string): Array<ParsedPrometheusMetric> {\n return parsePrometheusTextFormat(text).map((metric) => ({\n name: metric.name,\n help: metric.help || \"\",\n type: metric.type.toLowerCase(),\n metrics: metric.metrics.map((m) => ({\n value: Number(m.value),\n ...(m.labels && Object.keys(m.labels).length > 0 ? { labels: m.labels } : {}),\n })),\n }));\n}\n\nexport class PrometheusMetrics {\n private constructor(private readonly metrics: Array<ParsedPrometheusMetric>) {}\n\n static parse(maybePrometheusMetricsText: string): PrometheusMetrics {\n return new PrometheusMetrics(parsePrometheusText(maybePrometheusMetricsText));\n }\n\n /**\n * Gets all metrics of a specific name\n * @param name Metric name\n * @returns Array of metrics or undefined if not found\n * @example\n * ```ts\n * const metrics = parser.get('ponder_historical_total_indexing_seconds');\n * // Returns: [\n * // { value: 251224935, labels: { network: \"1\" } },\n * // { value: 251224935, labels: { network: \"8453\" } }\n * // ]\n * ```\n */\n get(name: string): Array<{ value: number; labels?: Record<string, string> }> | undefined {\n const metric = this.metrics.find((m) => m.name === name);\n return metric?.metrics;\n }\n\n /**\n * Gets a single metric value, optionally filtered by labels\n * @param name Metric name\n * @param labelFilter Optional label key-value pairs to match\n * @returns Metric value or undefined if not found\n * @example\n * ```ts\n * // Get simple value\n * parser.getValue('ponder_historical_start_timestamp_seconds') // Returns: 1740391265\n *\n * // Get value with label filter\n * parser.getValue('ponder_historical_total_indexing_seconds', { network: '1' }) // Returns: 251224935\n * ```\n */\n getValue(name: string, labelFilter?: Record<string, string>): number | undefined {\n const metrics = this.get(name);\n\n if (!metrics || metrics.length === 0) {\n return undefined;\n }\n\n if (!labelFilter) {\n return metrics[0]?.value;\n }\n\n const metric = metrics.find(\n (m) => m.labels && Object.entries(labelFilter).every(([k, v]) => m.labels?.[k] === v),\n );\n\n return metric?.value;\n }\n\n /**\n * Gets a label value from a metric\n * @param name Metric name\n * @param label Label name to retrieve\n * @returns Label value or undefined if not found\n * @example\n * ```ts\n * parser.getLabel('ponder_version_info', 'version') // Returns: \"0.9.18\"\n * parser.getLabel('ponder_settings_info', 'ordering') // Returns: \"omnichain\"\n * ```\n */\n getLabel(name: string, label: string): string | undefined {\n return this.getLabels(name, label)[0];\n }\n\n /**\n * Gets all unique label values for a metric\n * @param name Metric name\n * @param label Label name to retrieve\n * @returns Array of unique label values\n * @example\n * ```ts\n * // Get all network IDs\n * parser.getLabels('ponder_historical_total_indexing_seconds', 'network')\n * // Returns: ['1', '8453']\n * ```\n */\n getLabels(name: string, label: string): string[] {\n const metrics = this.get(name);\n\n if (!metrics) return [];\n\n return [\n ...new Set(metrics.map((m) => m.labels?.[label]).filter((v): v is string => v !== undefined)),\n ];\n }\n\n /**\n * Gets help text for a metric\n * @param name Metric name\n * @returns Help text or undefined if not found\n * @example\n * ```ts\n * parser.getHelp('ponder_historical_start_timestamp_seconds')\n * // Returns: \"Timestamp at which historical indexing started\"\n * ```\n */\n getHelp(name: string): string | undefined {\n return this.metrics.find((m) => m.name === name)?.help;\n }\n\n /**\n * Gets metric type\n * @param name Metric name\n * @returns Metric type or undefined if not found\n * @example\n * ```ts\n * parser.getType('ponder_version_info') // Returns: \"gauge\"\n * parser.getType('ponder_postgres_query_total') // Returns: \"counter\"\n * ```\n */\n getType(name: string): string | undefined {\n return this.metrics.find((m) => m.name === name)?.type;\n }\n\n /**\n * Gets all metric names\n * @returns Array of metric names\n * @example\n * ```ts\n * parser.getMetricNames()\n * // Returns: [\n * // 'ponder_version_info',\n * // 'ponder_settings_info',\n * // 'ponder_historical_start_timestamp_seconds',\n * // 'ponder_historical_total_indexing_seconds'\n * // ]\n * ```\n */\n getMetricNames(): string[] {\n return this.metrics.map((m) => m.name);\n }\n}\n","import { MiddlewareHandler } from \"hono\";\nimport { ReadonlyDrizzle } from \"ponder\";\nimport { PublicClient } from \"viem\";\nimport { queryPonderMeta, queryPonderStatus } from \"./db-helpers\";\nimport { PrometheusMetrics } from \"./prometheus-metrics\";\nimport type { BlockMetadata, NetworkIndexingStatus } from \"./types/common\";\n\nexport function ponderMetadata({\n app,\n db,\n env,\n fetchPrometheusMetrics,\n publicClients,\n}: {\n db: ReadonlyDrizzle<Record<string, unknown>>;\n app: {\n name: string;\n version: string;\n };\n env: {\n DATABASE_SCHEMA: string;\n } & Record<string, unknown>;\n fetchPrometheusMetrics: () => Promise<string>;\n publicClients: Record<number, PublicClient>;\n}): MiddlewareHandler {\n return async function ponderMetadataMiddleware(ctx) {\n const indexedChainIds = Object.keys(publicClients).map(Number);\n\n const ponderStatus = await queryPonderStatus(env.DATABASE_SCHEMA, db);\n const ponderMeta = await queryPonderMeta(env.DATABASE_SCHEMA, db);\n const metrics = PrometheusMetrics.parse(await fetchPrometheusMetrics());\n\n const networkIndexingStatus: Record<string, NetworkIndexingStatus> = {};\n\n for (const indexedChainId of indexedChainIds) {\n const publicClient = publicClients[indexedChainId];\n\n if (!publicClient) {\n throw new Error(`No public client found for chainId ${indexedChainId}`);\n }\n\n const latestSafeBlock = await publicClient.getBlock();\n\n const network = indexedChainId.toString();\n const ponderStatusForNetwork = ponderStatus.find((s) => s.network_name === network);\n const lastIndexedBlock = mapPonderStatusBlockToBlockMetadata(ponderStatusForNetwork);\n\n networkIndexingStatus[network] = {\n totalBlocksCount:\n metrics.getValue(\"ponder_historical_total_blocks\", {\n network,\n }) ?? null,\n cachedBlocksCount:\n metrics.getValue(\"ponder_historical_cached_blocks\", {\n network,\n }) ?? null,\n lastSyncedBlock: blockInfo({\n number:\n metrics.getValue(\"ponder_sync_block\", {\n network,\n }) ?? 0,\n timestamp: 0,\n }),\n latestSafeBlock: blockInfo({\n number: Number(latestSafeBlock.number),\n timestamp: Number(latestSafeBlock.timestamp),\n }),\n lastIndexedBlock,\n isRealtime: Boolean(metrics.getValue(\"ponder_sync_is_realtime\", { network })),\n isComplete: Boolean(metrics.getValue(\"ponder_sync_is_complete\", { network })),\n isQueued: lastIndexedBlock === null,\n } satisfies NetworkIndexingStatus;\n }\n\n return ctx.json({\n app,\n // application dependencies version\n deps: {\n ponder: metrics.getLabel(\"ponder_version_info\", \"version\"),\n nodejs: metrics.getLabel(\"nodejs_version_info\", \"version\"),\n },\n // application environment variables\n env,\n // application runtime information\n runtime: {\n // application build id\n // https://github.com/ponder-sh/ponder/blob/626e524/packages/core/src/build/index.ts#L425-L431\n codebaseBuildId: ponderMeta?.build_id,\n // tableNames: meta?.table_names,\n networkIndexingStatus,\n },\n });\n };\n}\n\nfunction blockInfo(block: {\n number: number | null;\n timestamp: number | null;\n}): BlockMetadata | null {\n if (!block.number) {\n return null;\n }\n\n return {\n height: block.number,\n timestamp: block.timestamp ?? 0,\n utc: block.timestamp ? new Date(block.timestamp * 1000).toISOString() : \"\",\n };\n}\n\nfunction mapPonderStatusBlockToBlockMetadata(\n block:\n | {\n block_number: number | null;\n block_timestamp: number | null;\n }\n | undefined,\n): BlockMetadata | null {\n if (!block?.block_number) {\n return null;\n }\n\n return blockInfo({\n number: block.block_number,\n timestamp: block.block_timestamp,\n });\n}\n"],"mappings":"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","names":["_a"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ensnode/ponder-metadata",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A Hono middleware for making Ponder app metadata available to clients.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/namehash/ensnode.git",
|
|
10
|
+
"directory": "packages/ponder-metadata-api"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/namehash/ensnode/tree/main/packages/ponder-metadata-api",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"Ponder"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
"./middleware": {
|
|
18
|
+
"types": "./dist/middleware.d.ts",
|
|
19
|
+
"default": "./dist/middleware.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"drizzle-orm": "^0.39.3",
|
|
30
|
+
"parse-prometheus-text-format": "^1.1.1",
|
|
31
|
+
"viem": "^2.22.13"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "^1.9.4",
|
|
35
|
+
"@ensnode/shared-configs": "",
|
|
36
|
+
"@types/node": "^20.10.0",
|
|
37
|
+
"hono": "^4.6.14",
|
|
38
|
+
"ponder": "^0.9.18",
|
|
39
|
+
"tsup": "^8.3.6",
|
|
40
|
+
"typescript": "^5.7.3",
|
|
41
|
+
"vitest": "^3.0.5"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"hono": "^4.6.14",
|
|
45
|
+
"ponder": "^0.9.18"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"prepublish": "tsup",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest",
|
|
51
|
+
"lint": "biome check --write",
|
|
52
|
+
"lint:ci": "biome ci"
|
|
53
|
+
},
|
|
54
|
+
"main": "./dist/middleware.js",
|
|
55
|
+
"module": "./dist/middleware.mjs",
|
|
56
|
+
"types": "./dist/middleware.d.ts"
|
|
57
|
+
}
|