diff --git a/packages/api/db/cargo.testing.sql b/packages/api/db/cargo.testing.sql index b009534833..0b21bfa2d8 100644 --- a/packages/api/db/cargo.testing.sql +++ b/packages/api/db/cargo.testing.sql @@ -48,5 +48,4 @@ INSERT INTO cargo.deals ("deal_id", "aggregate_cid", "client", "provider", "stat INSERT INTO public."user" (magic_link_id, github_id, name, email, public_address) VALUES ('did:ethr:0x65007A739ab7AC5c537161249b81250E49e2853Z', 'github|000000', 'mock user', 'test@gmail.com', '0x65007A739ab7AC5c537161249b81250E49e2853Z'); -INSERT INTO public.auth_key (name, secret, user_id) VALUES ('main', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDY1MDA3QTczOWFiN0FDNWM1MzcxNjEyNDliODEyNTBFNDllMjg1M0MiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTYzOTc1NDczNjYzOCwibmFtZSI6Im1haW4ifQ.wKwJIRXXHsgwVp8mOQp6r3_F4Lz5lnoAkgVP8wqwA_Y', 1); - +INSERT INTO public.auth_key (name, secret, user_id) VALUES ('main', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDY1MDA3QTczOWFiN0FDNWM1MzcxNjEyNDliODEyNTBFNDllMjg1M0MiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTYzOTc1NDczNjYzOCwibmFtZSI6Im1haW4ifQ.wKwJIRXXHsgwVp8mOQp6r3_F4Lz5lnoAkgVP8wqwA_Y', 1); \ No newline at end of file diff --git a/packages/api/src/index.js b/packages/api/src/index.js index f574fe2a31..586e90e7f7 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -3,7 +3,7 @@ import { notFound } from './utils/utils.js' import { HTTPError } from './errors.js' import { cors, postCors } from './routes/cors.js' import { JSONResponse } from './utils/json-response.js' -import { metrics } from './routes/metrics.js' +import { getStats, metrics } from './routes/metrics.js' import { tokensDelete } from './routes/tokens-delete.js' import { tokensCreate } from './routes/tokens-create.js' import { tokensList } from './routes/tokens-list.js' @@ -45,6 +45,7 @@ const r = new Router(getContext, { // Monitoring r.add('get', '/metrics', withMode(metrics, RO)) +r.add('get', '/stats', withMode(getStats, RO)) // CORS r.add('options', '*', cors) diff --git a/packages/api/src/routes/metrics.js b/packages/api/src/routes/metrics.js index 292cd0577a..65a88bccc2 100644 --- a/packages/api/src/routes/metrics.js +++ b/packages/api/src/routes/metrics.js @@ -1,4 +1,5 @@ import { UPLOAD_TYPES, PIN_SERVICES, PIN_STATUSES } from '../utils/db-client.js' +import { JSONResponse } from '../utils/json-response.js' /** * TODO: basic auth @@ -8,6 +9,19 @@ export async function metrics(_, { db }) { return new Response(await exportPromMetrics(db)) } +/** @type {import('../bindings').Handler} */ +export async function getStats(_, { db }) { + let stats = await db.getStats() + + return new JSONResponse( + { + ok: true, + data: stats, + }, + { status: 200 } + ) +} + /** * Exports metrics in prometheus exposition format. * https://prometheus.io/docs/instrumenting/exposition_formats/ diff --git a/packages/api/src/utils/db-client.js b/packages/api/src/utils/db-client.js index 1db93d3aae..23f423ecc3 100644 --- a/packages/api/src/utils/db-client.js +++ b/packages/api/src/utils/db-client.js @@ -447,6 +447,36 @@ export class DBClient { return data[0].value } + + async getStats() { + /** @type {PostgrestQueryBuilder} */ + const query = this.client.from('metric') + const { data, error } = await query + .select('name, value') + .in('name', [ + 'deals_total', + 'deals_size_total', + 'uploads_past_7_total', + 'uploads_blob_total', + 'uploads_car_total', + 'uploads_nft_total', + 'uploads_remote_total', + 'uploads_multipart_total', + ]) + + if (error) { + throw new DBError(error) + } + + if (!data || !data.length) { + return undefined + } + + return data.reduce((obj, curr) => { + obj[curr.name] = curr.value + return obj + }, {}) + } } export class DBError extends Error { diff --git a/packages/api/src/utils/db-types.d.ts b/packages/api/src/utils/db-types.d.ts index 1e016544a3..e3e4842b13 100644 --- a/packages/api/src/utils/db-types.d.ts +++ b/packages/api/src/utils/db-types.d.ts @@ -309,6 +309,18 @@ export interface paths { } } } + '/stats': { + get: { + responses: { + /** OK */ + 200: { + schema: definitions['upload_stats'][] + } + /** Partial Content */ + 206: unknown + } + } + } '/metric': { get: { parameters: { @@ -857,6 +869,9 @@ export interface definitions { /** Format: timestamp with time zone */ deleted_at?: string } + upload_stats: { + [key: string]: string + } backup: { /** * Format: bigint diff --git a/packages/cron/src/jobs/metrics.js b/packages/cron/src/jobs/metrics.js index 56116185fe..c1fa4b0c44 100644 --- a/packages/cron/src/jobs/metrics.js +++ b/packages/cron/src/jobs/metrics.js @@ -17,6 +17,14 @@ const COUNT_USERS = 'SELECT COUNT(*) AS total FROM public.user' const COUNT_UPLOADS = 'SELECT COUNT(*) AS total FROM upload WHERE type = $1' +const UPLOADS_PAST_7_TOTAL = + 'SELECT COUNT(*) FROM upload WHERE inserted_at > CURRENT_DATE - 7' + +const DEALS_TOTAL = 'SELECT COUNT(*) from cargo.deals' + +const DEALS_SIZE_TOTAL = + 'SELECT SUM(export_size) as deals_size_total from cargo.aggregates' + const COUNT_PINS = 'SELECT COUNT(*) AS total FROM pin WHERE service = $1 AND status = $2' @@ -45,6 +53,11 @@ export async function updateMetrics({ roPg, rwPg }) { updateUploadsCount(roPg, rwPg, t) ) ), + withTimeLog('updateTotalUploadPast7', () => + updateTotalUploadPast7(roPg, rwPg) + ), + withTimeLog('updateTotalDeals', () => updateTotalDeals(roPg, rwPg)), + withTimeLog('updateTotalDealsSize', () => updateTotalDealsSize(roPg, rwPg)), ...PIN_SERVICES.map((svc) => PIN_STATUSES.map((s) => withTimeLog(`updatePinsCount[${svc}][${s}]`, () => @@ -85,6 +98,39 @@ async function updateUsersCount(roPg, rwPg) { await rwPg.query(UPDATE_METRIC, ['users_total', rows[0].total]) } +/** + * @param {Client} roPg + * @param {Client} rwPg + */ +async function updateTotalUploadPast7(roPg, rwPg) { + const { rows } = await roPg.query(UPLOADS_PAST_7_TOTAL) + if (!rows.length) throw new Error('no rows returned counting uploads') + await rwPg.query(UPDATE_METRIC, ['uploads_past_7_total', rows[0].count]) +} + +/** + * @param {Client} roPg + * @param {Client} rwPg + */ +async function updateTotalDeals(roPg, rwPg) { + const { rows } = await roPg.query(DEALS_TOTAL) + if (!rows.length) throw new Error(`no rows returned counting total deals`) + await rwPg.query(UPDATE_METRIC, [`deals_total`, rows[0].count]) +} + +/** + * @param {Client} roPg + * @param {Client} rwPg + */ +async function updateTotalDealsSize(roPg, rwPg) { + const { rows } = await roPg.query(DEALS_SIZE_TOTAL) + if (!rows.length) throw new Error(`no rows returned counting total deal size`) + await rwPg.query(UPDATE_METRIC, [ + `deals_size_total`, + rows[0].deals_size_total, + ]) +} + /** * @param {Client} roPg * @param {Client} rwPg